diff --git a/lldb/include/lldb/DataFormatters/FormattersHelpers.h b/lldb/include/lldb/DataFormatters/FormattersHelpers.h --- a/lldb/include/lldb/DataFormatters/FormattersHelpers.h +++ b/lldb/include/lldb/DataFormatters/FormattersHelpers.h @@ -58,8 +58,6 @@ lldb::ValueObjectSP GetValueOfLibCXXCompressedPair(ValueObject &pair); -time_t GetOSXEpoch(); - struct InferiorSizedWord { InferiorSizedWord(const InferiorSizedWord &word) : ptr_size(word.ptr_size) { diff --git a/lldb/source/Plugins/Language/ObjC/CF.cpp b/lldb/source/Plugins/Language/ObjC/CF.cpp --- a/lldb/source/Plugins/Language/ObjC/CF.cpp +++ b/lldb/source/Plugins/Language/ObjC/CF.cpp @@ -8,6 +8,7 @@ #include "CF.h" +#include "Plugins/Language/ObjC/Utilities.h" #include "Plugins/TypeSystem/Clang/TypeSystemClang.h" #include "lldb/Core/ValueObject.h" #include "lldb/Core/ValueObjectConstResult.h" diff --git a/lldb/source/Plugins/Language/ObjC/Cocoa.cpp b/lldb/source/Plugins/Language/ObjC/Cocoa.cpp --- a/lldb/source/Plugins/Language/ObjC/Cocoa.cpp +++ b/lldb/source/Plugins/Language/ObjC/Cocoa.cpp @@ -8,6 +8,7 @@ #include "Cocoa.h" +#include "Plugins/Language/ObjC/Utilities.h" #include "Plugins/TypeSystem/Clang/TypeSystemClang.h" #include "lldb/Core/Mangled.h" #include "lldb/Core/ValueObject.h" @@ -27,11 +28,14 @@ #include "llvm/ADT/APInt.h" #include "llvm/ADT/bit.h" +#include "llvm/Support/CheckedArithmetic.h" #include "Plugins/LanguageRuntime/ObjC/AppleObjCRuntime/AppleObjCRuntime.h" #include "NSString.h" +#include + using namespace lldb; using namespace lldb_private; using namespace lldb_private::formatters; @@ -785,6 +789,64 @@ return llvm::bit_cast(decodedBits); } +// POSIX has an epoch on Jan-1-1970, but Cocoa prefers Jan-1-2001 +// this call gives the POSIX equivalent of the Cocoa epoch +time_t lldb_private::formatters::GetOSXEpoch() { + static time_t epoch = 0; +#ifdef __APPLE__ + if (!epoch) { + tzset(); + tm tm_epoch; + tm_epoch.tm_sec = 0; + tm_epoch.tm_hour = 0; + tm_epoch.tm_min = 0; + tm_epoch.tm_mon = 0; + tm_epoch.tm_mday = 1; + tm_epoch.tm_year = 2001 - 1900; + tm_epoch.tm_isdst = -1; + tm_epoch.tm_gmtoff = 0; + tm_epoch.tm_zone = nullptr; + epoch = timegm(&tm_epoch); + } +#endif // __APPLE__ + return epoch; +} + +bool lldb_private::formatters::NSDate::FormatDateValue(double date_value, + Stream &stream) { + if (date_value == -63114076800) { + stream.Printf("0001-12-30 00:00:00 +0000"); + return true; + } + + // Rely on implementation-defined behavior from std::llrint to clamp an + // out-of-range `double` value to a value representable by `time_t` without + // crashing LLDB with a floating-point exception. + // + // We're not distinguishing between the case where date_value "is" the max + // `time_t` value, and the case where std::llrint simply returns that value + // due to clamping, although technically we should be able to by inspecting + // the `math_errhandling` macro (which is set on a domain/overflow error). + const time_t date_value_as_time_t = + static_cast(std::llrint(date_value)); + time_t epoch = GetOSXEpoch(); + if (auto osx_epoch = llvm::checkedAdd(epoch, date_value_as_time_t)) + epoch = *osx_epoch; + else + return false; + + tm *tm_date = gmtime(&epoch); + if (!tm_date) + return false; + std::string buffer(1024, 0); + if (strftime(&buffer[0], 1023, "%Z", tm_date) == 0) + return false; + stream.Printf("%04d-%02d-%02d %02d:%02d:%02d %s", tm_date->tm_year + 1900, + tm_date->tm_mon + 1, tm_date->tm_mday, tm_date->tm_hour, + tm_date->tm_min, tm_date->tm_sec, buffer.c_str()); + return true; +} + bool lldb_private::formatters::NSDateSummaryProvider( ValueObject &valobj, Stream &stream, const TypeSummaryOptions &options) { ProcessSP process_sp = valobj.GetProcessSP(); @@ -828,6 +890,16 @@ if (descriptor->GetTaggedPointerInfo(&info_bits, &value_bits)) { date_value_bits = ((value_bits << 8) | (info_bits << 4)); memcpy(&date_value, &date_value_bits, sizeof(date_value_bits)); + + // Accomodate the __NSTaggedDate format introduced in Foundation 1600. + if (class_name == g___NSTaggedDate) { + auto *apple_runtime = llvm::dyn_cast_or_null( + ObjCLanguageRuntime::Get(*process_sp)); + if (!apple_runtime) + return false; + if (apple_runtime->GetFoundationVersion() >= 1600) + date_value = decodeTaggedTimeInterval(value_bits << 4); + } } else { llvm::Triple triple( process_sp->GetTarget().GetArchitecture().GetTriple()); @@ -850,34 +922,7 @@ } else return false; - if (date_value == -63114076800) { - stream.Printf("0001-12-30 00:00:00 +0000"); - return true; - } - - // Accomodate for the __NSTaggedDate format introduced in Foundation 1600. - if (class_name == g___NSTaggedDate) { - auto *runtime = llvm::dyn_cast_or_null( - ObjCLanguageRuntime::Get(*process_sp)); - if (runtime && runtime->GetFoundationVersion() >= 1600) - date_value = decodeTaggedTimeInterval(value_bits << 4); - } - - // this snippet of code assumes that time_t == seconds since Jan-1-1970 this - // is generally true and POSIXly happy, but might break if a library vendor - // decides to get creative - time_t epoch = GetOSXEpoch(); - epoch = epoch + (time_t)date_value; - tm *tm_date = gmtime(&epoch); - if (!tm_date) - return false; - std::string buffer(1024, 0); - if (strftime(&buffer[0], 1023, "%Z", tm_date) == 0) - return false; - stream.Printf("%04d-%02d-%02d %02d:%02d:%02d %s", tm_date->tm_year + 1900, - tm_date->tm_mon + 1, tm_date->tm_mday, tm_date->tm_hour, - tm_date->tm_min, tm_date->tm_sec, buffer.c_str()); - return true; + return NSDate::FormatDateValue(date_value, stream); } bool lldb_private::formatters::ObjCClassSummaryProvider( @@ -1098,29 +1143,6 @@ return true; } -// POSIX has an epoch on Jan-1-1970, but Cocoa prefers Jan-1-2001 -// this call gives the POSIX equivalent of the Cocoa epoch -time_t lldb_private::formatters::GetOSXEpoch() { - static time_t epoch = 0; - if (!epoch) { -#ifndef _WIN32 - tzset(); - tm tm_epoch; - tm_epoch.tm_sec = 0; - tm_epoch.tm_hour = 0; - tm_epoch.tm_min = 0; - tm_epoch.tm_mon = 0; - tm_epoch.tm_mday = 1; - tm_epoch.tm_year = 2001 - 1900; - tm_epoch.tm_isdst = -1; - tm_epoch.tm_gmtoff = 0; - tm_epoch.tm_zone = nullptr; - epoch = timegm(&tm_epoch); -#endif - } - return epoch; -} - template bool lldb_private::formatters::NSDataSummaryProvider( ValueObject &, Stream &, const TypeSummaryOptions &); diff --git a/lldb/source/Plugins/Language/ObjC/Utilities.h b/lldb/source/Plugins/Language/ObjC/Utilities.h new file mode 100644 --- /dev/null +++ b/lldb/source/Plugins/Language/ObjC/Utilities.h @@ -0,0 +1,32 @@ +//===-- Utilities.h ---------------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLDB_PLUGINS_LANGUAGE_OBJC_UTILITIES_H +#define LLDB_PLUGINS_LANGUAGE_OBJC_UTILITIES_H + +#include + +namespace lldb_private { + +class Stream; + +namespace formatters { + +/// Return an epoch time_t adjusted to the reference date set by the Cocoa +/// framework. +time_t GetOSXEpoch(); + +namespace NSDate { +/// Format the date_value field of a NSDate. +bool FormatDateValue(double date_value, Stream &stream); +} // namespace NSDate + +} // namespace formatters +} // namespace lldb_private + +#endif // LLDB_PLUGINS_LANGUAGE_OBJC_UTILITIES_H diff --git a/lldb/unittests/Language/AppleObjC/CMakeLists.txt b/lldb/unittests/Language/AppleObjC/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/lldb/unittests/Language/AppleObjC/CMakeLists.txt @@ -0,0 +1,6 @@ +add_lldb_unittest(LanguageAppleObjCTests + UtilitiesTests.cpp + + LINK_LIBS + lldbPluginObjCLanguage + ) diff --git a/lldb/unittests/Language/AppleObjC/UtilitiesTests.cpp b/lldb/unittests/Language/AppleObjC/UtilitiesTests.cpp new file mode 100644 --- /dev/null +++ b/lldb/unittests/Language/AppleObjC/UtilitiesTests.cpp @@ -0,0 +1,60 @@ +//===-- UtilitiesTests.cpp ------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "Plugins/Language/ObjC/Utilities.h" +#include "lldb/Utility/StreamString.h" +#include "llvm/ADT/Optional.h" +#include "llvm/ADT/StringRef.h" +#include "gtest/gtest.h" +#include +#include + +using namespace lldb_private; + +// Try to format the date_value field of a NSDate. +static llvm::Optional formatDateValue(double date_value) { + StreamString s; + bool succcess = formatters::NSDate::FormatDateValue(date_value, s); + if (succcess) + return std::string(s.GetData()); + return llvm::None; +} + +TEST(DataFormatterTest, NSDate) { + EXPECT_EQ(formatDateValue(-63114076800), + std::string("0001-12-30 00:00:00 +0000")); + + // Assert that std::llrint clamps values greater/lesser than the max/min + // time_t values. If it doesn't, it won't make sense to proceed, as we're + // relying on this behavior to avoid float-cast-overflow UB. + const double greater_than_max_time = 1e200; + const double lesser_than_min_time = -1e200; + ASSERT_LE(static_cast(std::llrint(greater_than_max_time)), + std::numeric_limits::max()); + ASSERT_GE(static_cast(std::llrint(lesser_than_min_time)), + std::numeric_limits::min()); + + // If the date_value `double` cannot be represented in a `time_t`, give up. + EXPECT_EQ(formatDateValue(greater_than_max_time), llvm::None); + EXPECT_EQ(formatDateValue(lesser_than_min_time), llvm::None); + + // If the date_value `double` plus the epoch cannot be represented in a + // `time_t`, give up. Try to craft a double that will trigger this scenario: + // if we can't do it, don't proceed. + const time_t date_value_to_trigger_overflow = + std::numeric_limits::max() - formatters::GetOSXEpoch() + 1; + const double date_value_to_trigger_overflow_as_double = + static_cast(date_value_to_trigger_overflow); + ASSERT_LE(date_value_to_trigger_overflow, + static_cast(date_value_to_trigger_overflow_as_double)); + EXPECT_EQ(formatDateValue(date_value_to_trigger_overflow_as_double), + llvm::None); + + EXPECT_EQ(formatDateValue(0), std::string("2001-01-01 00:00:00 UTC")); + EXPECT_EQ(formatDateValue(1024), std::string("2001-01-01 00:17:04 UTC")); +} diff --git a/lldb/unittests/Language/CMakeLists.txt b/lldb/unittests/Language/CMakeLists.txt --- a/lldb/unittests/Language/CMakeLists.txt +++ b/lldb/unittests/Language/CMakeLists.txt @@ -1,2 +1,6 @@ add_subdirectory(CPlusPlus) add_subdirectory(Highlighting) + +if (APPLE) + add_subdirectory(AppleObjC) +endif()