diff --git a/platform/unix/syspovctime.cpp b/platform/unix/syspovctime.cpp new file mode 100644 index 000000000..c327e557e --- /dev/null +++ b/platform/unix/syspovctime.cpp @@ -0,0 +1,228 @@ +//****************************************************************************** +/// +/// @file platform/unix/syspovctime.cpp +/// +/// Unix-specific implementation of ``-related functions. +/// +/// @copyright +/// @parblock +/// +/// Persistence of Vision Ray Tracer ('POV-Ray') version 3.8. +/// Copyright 1991-2021 Persistence of Vision Raytracer Pty. Ltd. +/// +/// POV-Ray is free software: you can redistribute it and/or modify +/// it under the terms of the GNU Affero General Public License as +/// published by the Free Software Foundation, either version 3 of the +/// License, or (at your option) any later version. +/// +/// POV-Ray is distributed in the hope that it will be useful, +/// but WITHOUT ANY WARRANTY; without even the implied warranty of +/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/// GNU Affero General Public License for more details. +/// +/// You should have received a copy of the GNU Affero General Public License +/// along with this program. If not, see . +/// +/// ---------------------------------------------------------------------------- +/// +/// POV-Ray is based on the popular DKB raytracer version 2.12. +/// DKBTrace was originally written by David K. Buck. +/// DKBTrace Ver 2.0-2.12 were written by David K. Buck & Aaron A. Collins. +/// +/// @endparblock +/// +//------------------------------------------------------------------------------ +// SPDX-License-Identifier: AGPL-3.0-or-later +//****************************************************************************** + +// Make sure the standard headers expose the required additional functionality +// not defined in the C++ standard. +#define _DEFAULT_SOURCE 1 // potentially needed for `timegm()` (if available at all) in glibc +#define _POSIX_C_SOURCE 1 // potentially needed for `gmtime_r()` and `localtime_r()` in glibc + +#include "syspovctime.h" + +// Standard C header files +#include // Deliberately including this as opposed to `` + +// C++ variants of standard C header files +#include + +// this must be the last file included +#include "base/povdebug.h" + +namespace pov_base +{ + +static void tzset_once() +{ + static bool alreadyRun = false; + if (alreadyRun) return; + alreadyRun = true; + tzset(); +} + +tm* safe_gmtime(const time_t* ts, tm* buf) +{ + // Any POSIX- or SUSv2-compliant system should have `gmtime_r()`, + // as should any system compliant with C11 (NB that's not C++11). + // Should you find your system not supporting this function, + // we would like to know about this so we can improve portability. + return gmtime_r(ts, buf); +} + +tm* safe_localtime(const time_t* ts, tm* buf) +{ + // Make sure tzset() has been run (in contrast to `std::localtime()`, + // `localtime_r()` is not required to do this). + tzset_once(); + // Any POSIX- or SUSv2-compliant system should have `localtime_r()`, + // as should any system compliant with C11 (NB that's not C++11). + // Should you find your system not supporting this function, + // we would like to know about this so we can improve portability. + return localtime_r(ts, buf); +} + +#if defined(HAVE_TIMEGM) + +time_t mktime_utc(tm* t) +{ + // GNU/BSD extension. + return timegm(t); +} + +#else // HAVE_TIMEGM + +// Verify that the specified `std::time_t` value corresponds to the specified date, 00:00 GMT +static bool checkTimeT(time_t date, int year, int month, int day) +{ + tm t; + (void)safe_gmtime(&date, &t); + return + (t.tm_year + 1900 == year) && + (t.tm_mon + 1 == month) && + (t.tm_mday == day) && + (t.tm_hour == 0) && + (t.tm_min == 0) && + (t.tm_sec == 0); +} + +// Convert from Gregorian date to a linear day number. +// NB: The month must be normalized, i.e. in the range [1..12]. Out-of-range day number are +// allowed though, and interpreted as relative to the specified month. +// NB: The current implementation returns the so-called Modified Julian Day, assigning day 0 to +// the Gregorian date 1858-11-17. However, this should be considered an implementation detail. +// NB: The current implementation is based on a formula for the (regular) Julian Day that breaks +// down at around 4800 BCE or earlier. +static int_least32_t LinearDay(int year, int month, int day) +{ + POV_ASSERT((month >= 1) && (month <= 12)); + int_least32_t julianDay = (1461 * (year + 4800 + (month - 14) / 12)) / 4 + + (367 * (month - 2 - 12 * ((month - 14) / 12))) / 12 - + (3 * ((year + 4900 + (month - 14) / 12) / 100)) / 4 + + day - 32075; + return julianDay - 2400000; +} + +// Implementation of `mktime_utc()` relying on POSIX-compliant `std::time_t`. +// This implementation uses direct math on `std::time_t`. +// NB: This function breaks down at around 4800 BCE or earlier. +time_t mktime_utc_impl_posix(tm* t) +{ + // To convert the given date into a linear day value, + // we must first normalize out-of-range month values. + if (t->tm_mon < 0) + { + int borrow = (11 - t->tm_mon) / 12; + t->tm_mon += borrow * 12; + t->tm_year -= borrow; + } + else if (t->tm_mon >= 12) + { + int carry = t->tm_mon / 12; + t->tm_mon -= carry * 12; + t->tm_year += carry; + } + + // Figure out the number of days between the start of the UNIX epoch and the date in question. + static const auto epochLinearDay = LinearDay(1970, 1, 1); + time_t result = LinearDay(t->tm_year + 1900, t->tm_mon + 1, t->tm_mday) - epochLinearDay; + + // Adding time is trivial, even if the fields are non-normalized. + result = result * 24 + t->tm_hour; + result = result * 60 + t->tm_min; + result = result * 60 + t->tm_sec; + + // To fully match the functionality of `std::mktime()`, normalize the input data. + (void)safe_gmtime(&result, t); + + return result; +} + +// Portable implementation of `mktime_utc()`. +// NB: On systems that use a strictly linear `std::time_t` keeping track of leap seconds, +// this function may be off by a second for any input calendar date and time that happens to +// fall within up to 24h (depending on locale) of any point in time when UTC is adjusted by such +// a leap second. +// NB: If at any time in the past the locale's time zone has ever been re-defined to use a +// different base offset from UTC, this function may be off by the corresponding difference for +// any input calendar date and time that happens to fall within up to 24h (depending on locale) +// of such an event. (The usual changes between DST and non-DST should not be a problem though.) +time_t mktime_utc_impl_fallback(tm* t) +{ + // As a first approximation, convert the date as if it was local (non-DST) time. + // (The algorithm would also work if we chose DST, but we must be consistent.) + t->tm_isdst = 0; // Make sure to explicitly choose non-DST. + auto localTime = mktime(t); + // Note that the resulting value is off by the local time zone offset during non-DST periods. + // To determine how much that is in `std::time_t` units, convert the resulting value back, + // but as UTC, then subject it to the same faulty conversion, and compute the difference to + // the initial result. + (void)safe_gmtime(&localTime, t); + t->tm_isdst = 0; // Make sure to explicitly choose non-DST again, as above. + auto timeOff = mktime(t) - localTime; + // Finally, correct the first approximation by the determined error. + auto result = localTime - timeOff; + + // To fully match the functionality of `std::mktime()`, normalize the input data. + // (Also, we quite messed it up by now, so we'd want to reconstruct it anyway.) + (void)safe_gmtime(&result, t); + + return result; +} + +time_t mktime_utc_impl_detect(tm* t); + +time_t(*mktime_utc_impl)(tm*) = &mktime_utc_impl_detect; + +// Invoke one of our own implementations of `mktime_utc()`, depending on `std::time_t` clock style. +time_t mktime_utc_impl_detect(tm* t) +{ + time_t (*chosenImpl)(tm* t) = &mktime_utc_impl_fallback; + + // Try whether we can recognize the `std::time_t` clock style. + if (checkTimeT(0, 1970, 1, 1)) + { + // UNIX epoch, i.e. 1970-01-01 00:00 UTC. + if (checkTimeT(946684800, 2000, 1, 1)) + // Genuine POSIX time, i.e. seconds elapsed since UNIX epoch, excluding leap seconds. + chosenImpl = &mktime_utc_impl_posix; + else if (checkTimeT(946684832, 2000, 1, 1)) + // TAI-based POSIX-style time, i.e. seconds elapsed since UNIX epoch, including leap seconds. + chosenImpl = &mktime_utc_impl_fallback; // Can't do proper math on leap seconds. + } + mktime_utc_impl = chosenImpl; + + // Delegate to whatever implementation we've chosen. + return mktime_utc_impl(t); +} + +time_t mktime_utc(tm* t) +{ + // One of our own implementations. + return mktime_utc_impl(t); +} + +#endif // HAVE_TIMEGM + +} // end of namespace pov_base diff --git a/platform/unix/syspovctime.h b/platform/unix/syspovctime.h new file mode 100644 index 000000000..517a7e2e5 --- /dev/null +++ b/platform/unix/syspovctime.h @@ -0,0 +1,67 @@ +//****************************************************************************** +/// +/// @file platform/unix/syspovctime.h +/// +/// Unix-specific declaration of ``-related functions. +/// +/// @copyright +/// @parblock +/// +/// Persistence of Vision Ray Tracer ('POV-Ray') version 3.8. +/// Copyright 1991-2021 Persistence of Vision Raytracer Pty. Ltd. +/// +/// POV-Ray is free software: you can redistribute it and/or modify +/// it under the terms of the GNU Affero General Public License as +/// published by the Free Software Foundation, either version 3 of the +/// License, or (at your option) any later version. +/// +/// POV-Ray is distributed in the hope that it will be useful, +/// but WITHOUT ANY WARRANTY; without even the implied warranty of +/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/// GNU Affero General Public License for more details. +/// +/// You should have received a copy of the GNU Affero General Public License +/// along with this program. If not, see . +/// +/// ---------------------------------------------------------------------------- +/// +/// POV-Ray is based on the popular DKB raytracer version 2.12. +/// DKBTrace was originally written by David K. Buck. +/// DKBTrace Ver 2.0-2.12 were written by David K. Buck & Aaron A. Collins. +/// +/// @endparblock +/// +//------------------------------------------------------------------------------ +// SPDX-License-Identifier: AGPL-3.0-or-later +//****************************************************************************** + +#ifndef POVRAY_UNIX_SYSPOVCTIME_H +#define POVRAY_UNIX_SYSPOVCTIME_H + +#include "base/configbase.h" + +#include + +namespace pov_base +{ + +/// Thread-Safe Replacement for `std::gmtime()`. +/// This function shall do the same job as `std::gmtime()`, except that it +/// shall use the supplied buffer to store the result, rather than a +/// static one. +std::tm* safe_gmtime(const std::time_t* ts, std::tm* buf); + +/// Thread-Safe Replacement for `std::localtime()`. +/// This function shall do the same job as `std::gmtime()`, except that it +/// shall use the supplied buffer to store the result, rather than a +/// static one. +std::tm* safe_localtime(const std::time_t* ts, std::tm* buf); + +/// UTC-Based Equivalent of `std::mktime()`. +/// This function shall do the same job as `std::mktime()`, except that it +/// shall interpret the input data as UTC. +std::time_t mktime_utc(std::tm* t); + +} // end of namespace pov_base + +#endif // POVRAY_UNIX_SYSPOVCTIME_H diff --git a/platform/windows/syspovctime.cpp b/platform/windows/syspovctime.cpp new file mode 100644 index 000000000..c8e89ceaa --- /dev/null +++ b/platform/windows/syspovctime.cpp @@ -0,0 +1,70 @@ +//****************************************************************************** +/// +/// @file platform/windows/syspovctime.cpp +/// +/// Windows-specific implementation of ``-related functions. +/// +/// @copyright +/// @parblock +/// +/// Persistence of Vision Ray Tracer ('POV-Ray') version 3.8. +/// Copyright 1991-2021 Persistence of Vision Raytracer Pty. Ltd. +/// +/// POV-Ray is free software: you can redistribute it and/or modify +/// it under the terms of the GNU Affero General Public License as +/// published by the Free Software Foundation, either version 3 of the +/// License, or (at your option) any later version. +/// +/// POV-Ray is distributed in the hope that it will be useful, +/// but WITHOUT ANY WARRANTY; without even the implied warranty of +/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/// GNU Affero General Public License for more details. +/// +/// You should have received a copy of the GNU Affero General Public License +/// along with this program. If not, see . +/// +/// ---------------------------------------------------------------------------- +/// +/// POV-Ray is based on the popular DKB raytracer version 2.12. +/// DKBTrace was originally written by David K. Buck. +/// DKBTrace Ver 2.0-2.12 were written by David K. Buck & Aaron A. Collins. +/// +/// @endparblock +/// +//------------------------------------------------------------------------------ +// SPDX-License-Identifier: AGPL-3.0-or-later +//****************************************************************************** + +#include "syspovctime.h" + +#include "base/pov_err.h" +#include "base/types.h" + +// this must be the last file included +#include "base/povdebug.h" + +namespace pov_base +{ + +std::tm* safe_gmtime(const std::time_t* ts, std::tm* buf) +{ + auto err = gmtime_s(buf, ts); + if (err != 0) + throw POV_EXCEPTION_CODE(kParamErr); + return buf; +} + +std::tm* safe_localtime(const std::time_t* ts, std::tm* buf) +{ + auto err = localtime_s(buf, ts); + if (err != 0) + throw POV_EXCEPTION_CODE(kParamErr); + return buf; +} + +std::time_t mktime_utc(std::tm* t) +{ + return _mkgmtime(t); +} + +} // end of namespace pov_base diff --git a/platform/windows/syspovctime.h b/platform/windows/syspovctime.h new file mode 100644 index 000000000..b6628e925 --- /dev/null +++ b/platform/windows/syspovctime.h @@ -0,0 +1,67 @@ +//****************************************************************************** +/// +/// @file platform/windows/syspovctime.h +/// +/// Windows-specific declaration of ``-related functions. +/// +/// @copyright +/// @parblock +/// +/// Persistence of Vision Ray Tracer ('POV-Ray') version 3.8. +/// Copyright 1991-2021 Persistence of Vision Raytracer Pty. Ltd. +/// +/// POV-Ray is free software: you can redistribute it and/or modify +/// it under the terms of the GNU Affero General Public License as +/// published by the Free Software Foundation, either version 3 of the +/// License, or (at your option) any later version. +/// +/// POV-Ray is distributed in the hope that it will be useful, +/// but WITHOUT ANY WARRANTY; without even the implied warranty of +/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/// GNU Affero General Public License for more details. +/// +/// You should have received a copy of the GNU Affero General Public License +/// along with this program. If not, see . +/// +/// ---------------------------------------------------------------------------- +/// +/// POV-Ray is based on the popular DKB raytracer version 2.12. +/// DKBTrace was originally written by David K. Buck. +/// DKBTrace Ver 2.0-2.12 were written by David K. Buck & Aaron A. Collins. +/// +/// @endparblock +/// +//------------------------------------------------------------------------------ +// SPDX-License-Identifier: AGPL-3.0-or-later +//****************************************************************************** + +#ifndef POVRAY_WINDOWS_SYSPOVCTIME_H +#define POVRAY_WINDOWS_SYSPOVCTIME_H + +#include "base/configbase.h" + +#include + +namespace pov_base +{ + +/// Thread-Safe Replacement for `std::gmtime()`. +/// This function shall do the same job as `std::gmtime()`, except that it +/// shall use the supplied buffer to store the result, rather than a +/// static one. +std::tm* safe_gmtime(const std::time_t* ts, std::tm* buf); + +/// Thread-Safe Replacement for `std::localtime()`. +/// This function shall do the same job as `std::gmtime()`, except that it +/// shall use the supplied buffer to store the result, rather than a +/// static one. +std::tm* safe_localtime(const std::time_t* ts, std::tm* buf); + +/// UTC-Based Equivalent of `std::mktime()`. +/// This function shall do the same job as `std::mktime()`, except that it +/// shall interpret the input data as UTC. +std::time_t mktime_utc(std::tm* t); + +} // end of namespace pov_base + +#endif // POVRAY_WINDOWS_SYSPOVCTIME_H diff --git a/source-doc/styleguide.md b/source-doc/styleguide.md index c4d50303a..4e500154a 100644 --- a/source-doc/styleguide.md +++ b/source-doc/styleguide.md @@ -264,6 +264,51 @@ To test whether a smart pointer is (non-)null, do not unnecessarily clutter your with `NULL`, as that would lead to compile errors on some (perfectly C++11 compliant) compilers. +Thread Safety +============= + +POV-Ray is a multithreaded application, and therefore all code should be written to be +thread-safe. While there may be instances where it makes sense to waive complete thread-safety +in favor of performance, all the more thought should go into such code, and those thoughts +well-documented, including an explanation of how the absence of thread-safety in the specific code +will not endanger the thread-safety of the application as a whole. + +Note that even some C++ standard library functions are explicitly not thread-safe. Most of such +functions are originally C functions adopted by the C++ standard. Such thread-unsafe functionality +**must not be called**, either directly or indirectly, **except** via the wrapper functions +defined in `source/base/threadsafe.h`. + +So far, we are aware of the following classes of functions have been identified to be problematic: + + - **Locale handling**: POV-Ray uses the `"C"` locale throughout, except for date/time related + aspects (`LC_TIME`), for which it uses the default (`""`) locale. The function + `std::setlocale()` **must not be invoked** under any circumstance. If your code needs the + locale to be switched to anything else, consider it broken, and search for an alternative. + - ****: Many functions in the `` header (aka `` in C) are not thread-safe, + and **must not** be invoked directly. If you are not _absolutely_ sure whether a function in + this header file is thread-safe, chances are it is _not_. Also, when consulting sources about + a function's thread safety, make sure your sources are authoritative: Just because a POSIX + operating system manual asserts that a function is thread-safe doesn't necessarily mean that + this is universally guaranteed by the C/C++ standard. + +The above rules are non-negotiable, as violating them _will_ cause threads to step on each others' +toes. + +@attention + Any instances of existing POV-Ray code violating this coding standard are just instances of + legacy code that haven't been overhauled yet, and - fortunately - haven't caused actual issues + in the wild... yet. Don't take those as role models for new code. + +@attention + Some libraries, such as boost, may provoide the same or similar functionalities as the above + problematic classes of functions, but promise to do so in a thread-safe manner. Be wary of + such promises, and carefully review the actual implementation, as they might actually be doing + essentially the same as `source/base/threadsafe.h`, namely providing guarded entry points to + the same underlying thread-unsafe functions, while relying on being the only entry points ever + used. Libraries built on that principle _will_ undermine the thread-safety of + `source/base/threadsafe.h` (and vice versa). + + Miscellaneous Coding Rules ========================== diff --git a/source/backend/povray.cpp b/source/backend/povray.cpp index c2e650dad..55cdd746e 100644 --- a/source/backend/povray.cpp +++ b/source/backend/povray.cpp @@ -39,6 +39,7 @@ #include "backend/povray.h" // C++ variants of standard C header files +#include #include // Boost header files @@ -674,6 +675,12 @@ boost::thread *povray_init(const boost::function0& threadExit, POVMSAddres POV_TerminateMainThread = false; POV_MainThreadTerminated = false; + // Standard "C" locale for all aspects, except as amended further below. + // (NB: Since this is the standard C/C++ default, it shouldn't matter, but it also won't hurt.) + std::setlocale(LC_ALL, "C"); + // User-preferred locale for date/time aspect. + std::setlocale(LC_TIME, ""); + Initialize_Noise(); pov::InitializePatternGenerators(); diff --git a/source/backend/scene/view.cpp b/source/backend/scene/view.cpp index 9af377316..8bb954ebf 100644 --- a/source/backend/scene/view.cpp +++ b/source/backend/scene/view.cpp @@ -39,13 +39,13 @@ #include "backend/scene/view.h" // C++ variants of standard C header files +// (none at the moment) // Standard C++ header files +#include // Boost header files #include -#include -#include #include #if POV_MULTITHREADED #include @@ -733,9 +733,11 @@ void View::StartRender(POVMS_Object& renderOptions) seed = renderOptions.TryGetInt(kPOVAttrib_StochasticSeed, 0); if (seed == 0) { - boost::posix_time::ptime now = boost::posix_time::second_clock::universal_time(); - boost::posix_time::ptime base = boost::posix_time::ptime(boost::gregorian::date(1970, 1, 1)); - seed = (now - base).total_seconds(); + // The following expression returns the number of _ticks_ elapsed since + // the system clock's _epoch_, where a _tick_ is the platform-dependent + // shortest time interval the system clock can measure, and _epoch_ is a + // platform-dependent point in time (usually 1970-01-01 00:00:00). + seed = std::chrono::system_clock::now().time_since_epoch().count(); } // TODO FIXME - [CLi] handle loading, storing (and later optionally deleting) of radiosity cache file for trace abort & continue feature diff --git a/source/base/image/metadata.h b/source/base/image/metadata.h index 63a7bd02c..ddb143641 100644 --- a/source/base/image/metadata.h +++ b/source/base/image/metadata.h @@ -8,7 +8,7 @@ /// @parblock /// /// Persistence of Vision Ray Tracer ('POV-Ray') version 3.8. -/// Copyright 1991-2018 Persistence of Vision Raytracer Pty. Ltd. +/// Copyright 1991-2021 Persistence of Vision Raytracer Pty. Ltd. /// /// POV-Ray is free software: you can redistribute it and/or modify /// it under the terms of the GNU Affero General Public License as @@ -31,6 +31,8 @@ /// /// @endparblock /// +//------------------------------------------------------------------------------ +// SPDX-License-Identifier: AGPL-3.0-or-later //****************************************************************************** #ifndef POVRAY_BASE_METADATA_H @@ -43,7 +45,6 @@ #include // Boost header files -#include #include namespace pov_base diff --git a/source/parser/parser.cpp b/source/parser/parser.cpp index b1a80be19..64f07c7fa 100644 --- a/source/parser/parser.cpp +++ b/source/parser/parser.cpp @@ -52,6 +52,9 @@ #include #include +// POV-Ray header files (platform module) +#include "syspovctime.h" + // POV-Ray header files (base module) #include "base/fileutil.h" #include "base/types.h" @@ -167,6 +170,17 @@ Parser::Parser(shared_ptr sd, bool useclk, DBL clk, size_t see next_rand(nullptr), Debug_Message_Buffer(messageFactory) { + std::tm tmY2k; + // Field = Value - Base + tmY2k.tm_year = 2000 - 1900; + tmY2k.tm_mon = 1 - 1; + tmY2k.tm_mday = 1 - 0; + tmY2k.tm_hour = 0; + tmY2k.tm_min = 0; + tmY2k.tm_sec = 0; + tmY2k.tm_isdst = 0; + mY2k = std::chrono::system_clock::from_time_t(pov_base::mktime_utc(&tmY2k)); + pre_init_tokenizer(); if (sceneData->realTimeRaytracing) mBetaFeatureFlags.realTimeRaytracing = true; @@ -205,6 +219,8 @@ void Parser::Run() // Initialize various defaults depending on language version as per command line / INI settings. InitDefaults(sceneData->EffectiveLanguageVersion()); + localTime = false; + Not_In_Default = true; Ok_To_Declare = true; LValue_Ok = false; @@ -7655,6 +7671,10 @@ void Parser::Parse_Global_Settings() Parse_End(); END_CASE + CASE (LOCAL_TIME_TOKEN) + localTime = ((int)Parse_Float() != 0); + END_CASE + OTHERWISE UNGET EXIT diff --git a/source/parser/parser.h b/source/parser/parser.h index 1f53b5771..108b67c3c 100644 --- a/source/parser/parser.h +++ b/source/parser/parser.h @@ -33,6 +33,8 @@ /// /// @endparblock /// +//------------------------------------------------------------------------------ +// SPDX-License-Identifier: AGPL-3.0-or-later //****************************************************************************** #ifndef POVRAY_PARSER_PARSE_H @@ -42,6 +44,7 @@ #include "parser/configparser.h" #include +#include #include "base/image/image.h" #include "base/messenger.h" @@ -703,6 +706,10 @@ class Parser : public SceneTask size_t MaxCachedMacroSize; + using FractionalDays = std::chrono::duration>; + std::chrono::system_clock::time_point mY2k; + bool localTime; + // parse.h/parse.cpp void Frame_Init(void); void InitDefaults(POVRayVersion version); diff --git a/source/parser/parser_expressions.cpp b/source/parser/parser_expressions.cpp index 0d85119cd..b9b146131 100644 --- a/source/parser/parser_expressions.cpp +++ b/source/parser/parser_expressions.cpp @@ -45,9 +45,6 @@ // Standard C++ header files #include -// Boost header files -#include - // POV-Ray header files (base module) #include "base/fileinputoutput.h" #include "base/mathutil.h" @@ -1112,45 +1109,7 @@ void Parser::Parse_Num_Factor (EXPRESS& Express,int *Terms) break; case NOW_TOKEN: - { - static boost::posix_time::ptime y2k(boost::gregorian::date(2000,1,1)); - boost::posix_time::ptime now(boost::posix_time::microsec_clock::universal_time()); - - // Due to a bug in `boost::posix_time::microsec_clock::universal_time()`, - // the value returned may actually be local time rather than UTC. We try to fix this - // by comparing with `boost::posix_time::second_clock::universal_time()`, which is - // less precise but more reliable in terms of time zone behavior. - // (NB: While we could theoretically compute the offset once, and then re-use it every time - // `new` is invoked, this would cause issues when rendering a scene during transition to or - // from daylight savings time, or other events affecting the time zone. The current approach - // is immune to such issues.) - boost::posix_time::ptime lowPrecisionNow(boost::posix_time::second_clock::universal_time()); - // The difference between the two timers, rounded to quarters of an hour, - // should correspond to the time zone offset (in seconds in this case). - const auto tzPrecisionInSeconds = 15 * 60; - int_least32_t tzOffset = std::lround(float((now - lowPrecisionNow).total_seconds()) / tzPrecisionInSeconds) * tzPrecisionInSeconds; - // Unless someone paused the code in between the above statements, the resulting difference - // should be our time zone offset in seconds (unless we're running on a system that's not - // subject to the bug, in which case the resulting difference should be zero). - if (tzOffset != 0) - { - if (sceneData->EffectiveLanguageVersion() < 380) - { - WarningOnce(kWarningGeneral, HERE, - "In POV-Ray v3.7, on some platforms 'now' erroneously evaluated to days since " - "2000-01-01 00:00 local time instead of UTC. For backward compatibility, this " - "bug is reproduced in legacy scenes, but the scene may produce different " - "results on other platforms."); - } - else - { - now -= boost::posix_time::time_duration(0, 0, tzOffset); - } - } - - const auto daysPerMicrosecond = 1.0e-6 / (24 * 60 * 60); - Val = (now - y2k).total_microseconds() * daysPerMicrosecond; - } + Val = std::chrono::duration_cast(std::chrono::system_clock::now() - mY2k).count(); break; } diff --git a/source/parser/parser_strings.cpp b/source/parser/parser_strings.cpp index 71030e9b5..8e13227b4 100644 --- a/source/parser/parser_strings.cpp +++ b/source/parser/parser_strings.cpp @@ -8,7 +8,7 @@ /// @parblock /// /// Persistence of Vision Ray Tracer ('POV-Ray') version 3.8. -/// Copyright 1991-2018 Persistence of Vision Raytracer Pty. Ltd. +/// Copyright 1991-2021 Persistence of Vision Raytracer Pty. Ltd. /// /// POV-Ray is free software: you can redistribute it and/or modify /// it under the terms of the GNU Affero General Public License as @@ -31,18 +31,24 @@ /// /// @endparblock /// +//------------------------------------------------------------------------------ +// SPDX-License-Identifier: AGPL-3.0-or-later //****************************************************************************** // Unit header file must be the first file included within POV-Ray *.cpp files (pulls in config) #include "parser/parser.h" -// C++ variants of C standard header files +// C++ variants of standard C header files #include #include #include +#include -// Boost header files -#include +// Standard C++ header files +#include + +// POV-Ray header files (platform module) +#include "syspovctime.h" // POV-Ray header files (core module) #include "core/scene/scenedata.h" @@ -468,25 +474,106 @@ UCS2 *Parser::Parse_Chr(bool /*pathname*/) * ******************************************************************************/ -#define PARSE_NOW_VAL_LENGTH 200 +static bool FindDatetimeFormatSpecifier(const char** list, const char* first, const char* last) +{ + POV_PARSER_ASSERT((*first != '\0') && (*last != '\0')); + + if (first == last) + return (std::strchr(list[0], *first) != nullptr); + + POV_PARSER_ASSERT(last - first == 1); + + for (int i = 1; list[i] != nullptr; ++i) + if (list[i][0] == *first) + return (std::strchr(list[i] + 1, *last) != nullptr); + + return false; +} UCS2 *Parser::Parse_Datetime(bool pathname) { + // Enum representing a type of placeholder offense. + enum OffenseType { kNoOffense, kLocaleSensitive, kTimezoneSensitive, kLegacyNonPortable, kNonPortable, kIncomplete }; + // Structure representing a placeholder and its offense. + struct Offense + { + // Offense, in ascending order of severity. + OffenseType type; + // Start of offending placeholder. + std::string field; // Offending placeholder + // Default Constructor. + Offense() : type(kNoOffense), field() {} + // Constructor. + Offense(OffenseType t, const char* first) : type(t), field(first) {} + // Constructor. + Offense(OffenseType t, const char* first, const char* last) : type(t), field(first, (last-first+1)) {} + // Overwrite data, provided other offense is more severe. + Offense& operator|=(const Offense& o) { if (o.type > type) { type = o.type; field = o.field; } return *this; } + // Convert to boolean + explicit operator bool() const { return type != kNoOffense; } + }; + + // Known prefixes of 2-character conversion specifiers. + // NB: `#` is a Microsoft extension. + static const char* knownPrefixes = "EO#"; + + // Conversion specifiers _guaranteed_ to be supported across _all_ platforms by specific versions of POV-Ray, + // as a nullptr-terminated list of strings. + // First string is a plain list of valid single-character specifiers; all other strings start with the respective + // prefix, followed by the corresponding list of valid secondary characters. + // NB: These lists should exactly match the set of conversion specifiers defined by the minimum C++ standard + // required to compile the respective version of POV-Ray. + + // POV-Ray v3.8, which requires C++11. + static const char* portableFieldsCurrent[] { + "aAbBcCdDeFgGhHIjmMnprRStTuUVwWxXyYzZ%", + "EcCxXyY", + "OdeHImMSuUVwWy", + nullptr }; + // POV-Ray v3.7, which required only C++03. + static const char* portableFieldsLegacy370[] { + "aAbBcdHIjmMpSUwWxXyYZ%", + nullptr }; + + // Conversion specifiers sensitive to time zone information (same format as above). + static const char* timezoneFields[] { "zZ", nullptr }; + + // Locale-independent conversion specifiers (same format as above). + // NB: The 2-character conversion specifiers defined in C/C++ are inherently locale-specific. + // NB: `z` qualifies in that its format is locale-independent, even though the value is not. + static const char* localeAgnosticFields[] { "CdDeFgGHIjmMnRStTuUVwWyYz%", nullptr }; + + // Fields that might evaluate to empty strings (same format as above). + // NB: `P` is a GNU extension. + static const char* potentiallyEmptyFields[] { "pPzZ", nullptr }; + + static constexpr int kMaxResultSize = 200; // Max number of chars in result, _including_ terminating NUL character. + char *FormatStr = nullptr; bool CallFree; + + Offense offense; int vlen = 0; - char val[PARSE_NOW_VAL_LENGTH + 1]; // Arbitrary size, usually a date format string is far less + char val[kMaxResultSize]; // Arbitrary size, usually a date format string is far less Parse_Paren_Begin(); - std::time_t timestamp = floor((Parse_Float() + (365*30+7)) * 24*60*60 + 0.5); + const char** portableFieldsLegacy; // Fields reliably supported by whatever POV-Ray version is specified via `#version`. + if (sceneData->EffectiveLanguageVersion() < 380) + portableFieldsLegacy = portableFieldsLegacy370; + else + portableFieldsLegacy = nullptr; // skip legacy portability check + + FractionalDays daysSinceY2k(Parse_Float()); Parse_Comma(); EXPECT_ONE CASE(RIGHT_PAREN_TOKEN) UNGET CallFree = false; - // we use GMT as some platforms (e.g. windows) have different ideas of what to print when handling '%z'. - FormatStr = (char *)"%Y-%m-%d %H:%M:%SZ"; + if (localTime) + FormatStr = "%Y-%m-%d %H:%M:%S%z"; + else + FormatStr = "%Y-%m-%d %H:%M:%SZ"; END_CASE OTHERWISE @@ -496,50 +583,211 @@ UCS2 *Parser::Parse_Datetime(bool pathname) if (FormatStr[0] == '\0') { POV_FREE(FormatStr); - Error("Empty format string."); + Error("Empty 'datetime' format string."); } - if (strlen(FormatStr) > PARSE_NOW_VAL_LENGTH) + for (const char* pc = FormatStr; *pc != '\0'; ++pc) { - POV_FREE(FormatStr); - Error("Format string too long."); + // Skip over any non-placeholders. + if (*pc != '%') + continue; + + // Keep track of start of placeholder, so we can more easily report it + // in case it turns out to be offensive. + const char* po = pc; + + if (*(++pc) == '\0') + { + offense |= Offense(kIncomplete, po); + break; + } + + // Keep track of first "payload" character of placeholder. + const char* pc1 = pc; + + // Check if we appear to have a two-character placeholder, and if so, + // advance to the next character. + if (std::strchr(knownPrefixes, *pc) != nullptr) + { + if (*(++pc) == '\0') + { + offense |= Offense(kIncomplete, po); + break; + } + } + + // NB: The next character should be the last one comprising the placeholder. + + if (offense.type >= kNonPortable) + continue; // We won't find any worse issue with this placeholder; next please. + + // Check if placeholder is portable across all platforms for this version of POV-Ray. + if (!FindDatetimeFormatSpecifier(portableFieldsCurrent, pc1, pc)) + offense |= Offense(kNonPortable, po, pc); + + if (offense.type >= kLegacyNonPortable) + continue; // We won't find any worse issue with this placeholder; next please. + + // Check if placeholder is portable across all platforms for the version the scene was designed for. + if ((portableFieldsLegacy != nullptr) && !FindDatetimeFormatSpecifier(portableFieldsLegacy, pc1, pc)) + offense |= Offense(kLegacyNonPortable, po, pc); + + if (offense.type >= kTimezoneSensitive) + continue; // We won't find any worse issue with this placeholder; next please. + + // Check if placeholder requires time zone information that we might not have. + if (!localTime && FindDatetimeFormatSpecifier(timezoneFields, pc1, pc)) + offense |= Offense(kTimezoneSensitive, po, pc); + + if (offense.type >= kLocaleSensitive) + continue; // We won't find any worse issue with this placeholder; next please. + + // Check if placeholder may have different format depending on locale. + if (!FindDatetimeFormatSpecifier(localeAgnosticFields, pc1, pc)) + offense |= Offense(kLocaleSensitive, po, pc); + } + switch (offense.type) + { + case kIncomplete: + Error("Incomplete 'datetime' format placeholder ('%s').", + offense.field.c_str()); + break; + + case kNonPortable: + WarningOnce(kWarningGeneral, HERE, + "Scene uses 'datetime' format placeholder(s) that may not be supported on all " + "platforms ('%s').", + offense.field.c_str()); + break; + + case kLegacyNonPortable: + WarningOnce(kWarningGeneral, HERE, + "Legacy scene uses 'datetime' format placeholder(s) that POV-Ray v%s " + "did not support on all platforms ('%s').", + sceneData->EffectiveLanguageVersion().str().c_str(), + offense.field.c_str()); + break; + + case kTimezoneSensitive: + WarningOnce(kWarningGeneral, HERE, + "Scene uses 'datetime' format placeholder(s) relating to time zone information " + "('%s') without 'local_time' global setting turned on, implying UTC mode of " + "operation. On some systems, this may cause the placeholder(s) to not work as " + "expected.", + offense.field.c_str()); + break; + + case kLocaleSensitive: + WarningOnce(kWarningGeneral, HERE, + "Scene uses 'datetime' format placeholder(s) that may produce " + "different results depending on system locale settings ('%s').", + offense.field.c_str()); + break; + + case kNoOffense: + break; } END_CASE END_EXPECT Parse_Paren_End(); - // NB don't wrap only the call to strftime() in the try, because visual C++ will, in release mode, - // optimize the try/catch away since it doesn't believe that the RTL can throw exceptions. since + // NB: Don't wrap only the call to strftime() in the try, because Visual C++ will, in release mode, + // optimize the try/catch away since it doesn't believe that the RTL can throw exceptions. Since // the windows version of POV hooks the invalid parameter handler RTL callback and throws an exception // if it's called, they can. try { - std::tm t = boost::posix_time::to_tm(boost::posix_time::from_time_t(timestamp)); - // TODO FIXME - we should either have this locale setting globally, or avoid it completely; in either case it shouldn't be *here*. - setlocale(LC_TIME,""); // Get the local preferred format - vlen = strftime(val, PARSE_NOW_VAL_LENGTH, FormatStr, &t); + auto timeSinceY2k = std::chrono::duration_cast(daysSinceY2k); + auto timeToPrint = mY2k + timeSinceY2k; + std::time_t timestamp = std::chrono::system_clock::to_time_t(timeToPrint); + + std::tm t; + if (localTime) + (void)safe_localtime(×tamp, &t); + else + (void)safe_gmtime(×tamp, &t); + + vlen = std::strftime(val, kMaxResultSize, FormatStr, &t); + + if (vlen == 0) + { + // As per the the C/C++ standard, a `strftime` return value of zero _per se_ may not + // necessarily indicate an error: The resulting string could simply be empty due to + // the format consisting of only conversion specifiers that happen to evaluate to + // empty strings for the current locale (e.g. `%p`). + + // We employ some strong logics as well as weaker heuristics to figure out whether + // this is definitely an error; in that case, we report it to code further downstream + // by setting the length value to the buffer size (which also happens to be how + // libc 4.4.1 and earlier reported overflow), which can never happen in case of a + // success. + + // TODO - we could avoid this whole mess by simply injecting some arbitrary character + // at the start or end of the format string, and cut it away again after the + // conversion. That way, even an "empty" string would give us at least 1 character. + + if (val[0] != '\0') + { + // In absence of an non-error, the result string would have to be empty, + // but that's not what we're seeing here, confirming that this is indeed + // unambiguously an error. + vlen = kMaxResultSize; + } + else + { + // Check if the result string could conceivably be empty. + for (const char* pc = FormatStr; *pc != '\0'; ++pc) + { + if (*pc != '%') + { + // Any non-placeholder character would have to be copied verbatim, + // and should therefore have resulted in a non-empty string. + vlen = kMaxResultSize; + break; + } + ++pc; + const char* pc1 = pc; + if (std::strchr(knownPrefixes, *pc) != nullptr) + ++pc; + if (!FindDatetimeFormatSpecifier(potentiallyEmptyFields, pc1, pc)) + { + // Placeholder is not in the list of those we expect to be replaced with empty strings. + vlen = kMaxResultSize; + break; + } + } + } + } } catch (pov_base::Exception& e) { - // the windows version of strftime calls the invalid parameter handler if - // it gets a bad format string. this will in turn raise an exception of type - // kParamErr. if the exception isn't that, allow normal exception processing + if (CallFree) + POV_FREE(FormatStr); + + // The windows version of `strftime` calls the invalid parameter handler if + // it gets a bad format string. This will in turn raise an exception of type + // `kParamErr`. If the exception isn't that, allow normal exception processing // to continue, otherwise we issue a more useful error message. if ((e.codevalid() == false) || (e.code() != kParamErr)) throw; - vlen = 0; + Error("Invalid 'datetime' format placeholder"); } - if (vlen == PARSE_NOW_VAL_LENGTH) // on error: max for libc 4.4.1 & before - vlen = 0; // return an empty string on error (content of val[] is undefined) - val[vlen]='\0'; // whatever, that operation is now safe (and superfluous except for error) - if (CallFree) - { POV_FREE(FormatStr); - } + + if (vlen == kMaxResultSize) + // Unambiguous error + Error("Invalid 'datetime' format placeholder, or resulting string too long (>%i characters).", + kMaxResultSize - 1); if (vlen == 0) - Error("Invalid formatting code in format string, or resulting string too long."); + { + // Maybe an error, maybe just an empty result string + PossibleError("'datetime' result string empty; if this is unexpected, check for " + "invalid 'datetime' format placeholders, or whether the resulting " + "string would have been too long (>%i characters).", + kMaxResultSize - 1); + } return String_To_UCS2(val); } diff --git a/source/parser/reservedwords.cpp b/source/parser/reservedwords.cpp index 0ce740564..8058d866b 100644 --- a/source/parser/reservedwords.cpp +++ b/source/parser/reservedwords.cpp @@ -33,6 +33,8 @@ /// /// @endparblock /// +//------------------------------------------------------------------------------ +// SPDX-License-Identifier: AGPL-3.0-or-later //****************************************************************************** // Unit header file must be the first file included within POV-Ray *.cpp files (pulls in config) @@ -304,6 +306,7 @@ const RESERVED_WORD Reserved_Words[] = { { LN_TOKEN, "ln" }, { LOAD_FILE_TOKEN, "load_file" }, { LOCAL_TOKEN, "local" }, + { LOCAL_TIME_TOKEN, "local_time", 380 }, { LOCATION_TOKEN, "location" }, { LOG_TOKEN, "log" }, { LOOK_AT_TOKEN, "look_at" }, diff --git a/source/parser/reservedwords.h b/source/parser/reservedwords.h index 1d7ffbee9..de0328d34 100644 --- a/source/parser/reservedwords.h +++ b/source/parser/reservedwords.h @@ -32,6 +32,8 @@ /// /// @endparblock /// +//------------------------------------------------------------------------------ +// SPDX-License-Identifier: AGPL-3.0-or-later //****************************************************************************** #ifndef POVRAY_PARSER_RESERVEDWORDS_H @@ -443,6 +445,7 @@ enum TOKEN_IDS LINEAR_SWEEP_TOKEN, LOAD_FILE_TOKEN, LOCAL_TOKEN, + LOCAL_TIME_TOKEN, LOCATION_TOKEN, LOOK_AT_TOKEN, LOOKS_LIKE_TOKEN, diff --git a/unix/configure.ac b/unix/configure.ac index 8687eefe2..4625be46d 100644 --- a/unix/configure.ac +++ b/unix/configure.ac @@ -645,6 +645,9 @@ AC_CHECK_DECLS([CLOCK_MONOTONIC, CLOCK_REALTIME, CLOCK_PROCESS_CPUTIME_ID, CLOCK ) AC_CHECK_TYPES([clockid_t], [], [], [[#include ]]) +# timegm +AC_CHECK_FUNCS([timegm]) + # getrusage and related AC_CHECK_FUNCS([getrusage]) AC_CHECK_DECLS([RUSAGE_SELF, RUSAGE_THREAD, RUSAGE_LWP], diff --git a/windows/pvengine.cpp b/windows/pvengine.cpp index 54bb5730e..10a071728 100644 --- a/windows/pvengine.cpp +++ b/windows/pvengine.cpp @@ -10,7 +10,7 @@ /// @parblock /// /// Persistence of Vision Ray Tracer ('POV-Ray') version 3.8. -/// Copyright 1991-2017 Persistence of Vision Raytracer Pty. Ltd. +/// Copyright 1991-2021 Persistence of Vision Raytracer Pty. Ltd. /// /// POV-Ray is free software: you can redistribute it and/or modify /// it under the terms of the GNU Affero General Public License as @@ -33,6 +33,8 @@ /// /// @endparblock /// +//------------------------------------------------------------------------------ +// SPDX-License-Identifier: AGPL-3.0-or-later //****************************************************************************** /// @file @@ -93,6 +95,7 @@ #endif #include "cpuid.h" +#include "syspovctime.h" #ifdef TRY_OPTIMIZED_NOISE // TODO - This is a hack; we should get the noise generator choice information via POVMS from the back-end. @@ -776,8 +779,10 @@ void PrintRenderTimes (int Finished, int NormalCompletion) char fn[_MAX_PATH]; char ts[256]; time_t t = time(NULL); - - strftime(ts, sizeof(ts), "%Y%m%d.%H%M%S", gmtime(&t)); + std::tm tm; + _locale_t loc = _create_locale(LC_TIME, "C"); + (void)pov_base::safe_gmtime(&t, &tm); + _strftime_l(ts, sizeof(ts), "%Y%m%d.%H%M%S", &tm, loc); sprintf(fn, "%sbenchmark-%s.txt", DocumentsPath, ts); FILE *f = fopen(fn, "wt"); if (f != NULL) @@ -785,7 +790,7 @@ void PrintRenderTimes (int Finished, int NormalCompletion) int n = Get_Benchmark_Version(); fprintf(f, "%s", str); - strftime(str, sizeof(str), "%#c", gmtime(&t)); + _strftime_l(str, sizeof(str), "%#c", &tm, loc); fprintf(f, "\nRender of benchmark version %x.%02x completed at %s UTC.\n", n / 256, n % 256, str); fprintf(f, "----------------------------------------------------------------------------\n"); GenerateDumpMeta(true); diff --git a/windows/vs2015/povplatform.vcxproj b/windows/vs2015/povplatform.vcxproj index 3deeba43f..a4c713b45 100644 --- a/windows/vs2015/povplatform.vcxproj +++ b/windows/vs2015/povplatform.vcxproj @@ -376,6 +376,7 @@ + @@ -424,6 +425,7 @@ + diff --git a/windows/vs2015/povplatform.vcxproj.filters b/windows/vs2015/povplatform.vcxproj.filters index 8776b02ff..8730a42cf 100644 --- a/windows/vs2015/povplatform.vcxproj.filters +++ b/windows/vs2015/povplatform.vcxproj.filters @@ -55,6 +55,9 @@ Platform Source\x86 + + Platform Source\Windows + @@ -90,5 +93,8 @@ Platform Headers\x86 + + Platform Headers\Windows + \ No newline at end of file