From 293964f69b8bc03fe837daf3688bfe4113fb9e74 Mon Sep 17 00:00:00 2001 From: Mark Mentovai Date: Sun, 3 Aug 2014 18:53:10 -0400 Subject: [PATCH] Add CFPropertyToLaunchData() and its test. TEST=util_test Launchd R=rsesek@chromium.org Review URL: https://codereview.chromium.org/438673003 --- util/mac/launchd.h | 39 ++++++ util/mac/launchd.mm | 137 ++++++++++++++++++ util/mac/launchd_test.mm | 292 +++++++++++++++++++++++++++++++++++++++ util/stdlib/objc.h | 39 ++++++ util/util.gyp | 4 + 5 files changed, 511 insertions(+) create mode 100644 util/mac/launchd.h create mode 100644 util/mac/launchd.mm create mode 100644 util/mac/launchd_test.mm create mode 100644 util/stdlib/objc.h diff --git a/util/mac/launchd.h b/util/mac/launchd.h new file mode 100644 index 00000000..f9876e2d --- /dev/null +++ b/util/mac/launchd.h @@ -0,0 +1,39 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef CRASHPAD_UTIL_MAC_LAUNCHD_H_ +#define CRASHPAD_UTIL_MAC_LAUNCHD_H_ + +#include +#include + +namespace crashpad { + +//! \brief Converts a Core Foundation-type property list to a launchd-type +//! `launch_data_t`. +//! +//! \param[in] property_cf The Core Foundation-type property list to convert. +//! +//! \return The converted launchd-type `launch_data_t`. The caller takes +//! ownership of the returned value. On error, returns `NULL`. +//! +//! \note This function handles all `CFPropertyListRef` types except for +//! `CFDateRef`, because there’s no `launch_data_type_t` analogue. Not all +//! types supported in a launchd-type `launch_data_t` have +//! `CFPropertyListRef` analogues. +launch_data_t CFPropertyToLaunchData(CFPropertyListRef property_cf); + +} // namespace crashpad + +#endif // CRASHPAD_UTIL_MAC_LAUNCHD_H_ diff --git a/util/mac/launchd.mm b/util/mac/launchd.mm new file mode 100644 index 00000000..a621f791 --- /dev/null +++ b/util/mac/launchd.mm @@ -0,0 +1,137 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "util/mac/launchd.h" + +#import + +#include "base/mac/foundation_util.h" +#include "base/mac/scoped_launch_data.h" +#include "base/mac/scoped_cftyperef.h" +#include "base/strings/sys_string_conversions.h" + +namespace crashpad { + +launch_data_t CFPropertyToLaunchData(CFPropertyListRef property_cf) { + // This function mixes Foundation and Core Foundation access to property list + // elements according to which is more convenient and correct for any specific + // task. + + launch_data_t data_launch = NULL; + CFTypeID type_id_cf = CFGetTypeID(property_cf); + + if (type_id_cf == CFDictionaryGetTypeID()) { + NSDictionary* dictionary_ns = base::mac::CFToNSCast( + base::mac::CFCastStrict(property_cf)); + base::mac::ScopedLaunchData dictionary_launch( + launch_data_alloc(LAUNCH_DATA_DICTIONARY)); + + for (NSString* key in dictionary_ns) { + if (![key isKindOfClass:[NSString class]]) { + return NULL; + } + + CFPropertyListRef value_cf = + static_cast([dictionary_ns objectForKey:key]); + launch_data_t value_launch = CFPropertyToLaunchData(value_cf); + if (!value_launch) { + return NULL; + } + + launch_data_dict_insert( + dictionary_launch, value_launch, [key UTF8String]); + } + + data_launch = dictionary_launch.release(); + + } else if (type_id_cf == CFArrayGetTypeID()) { + NSArray* array_ns = + base::mac::CFToNSCast(base::mac::CFCastStrict(property_cf)); + base::mac::ScopedLaunchData array_launch( + launch_data_alloc(LAUNCH_DATA_ARRAY)); + size_t index = 0; + + for (id element_ns in array_ns) { + CFPropertyListRef element_cf = static_cast(element_ns); + launch_data_t element_launch = CFPropertyToLaunchData(element_cf); + if (!element_launch) { + return NULL; + } + + launch_data_array_set_index(array_launch, element_launch, index++); + } + + data_launch = array_launch.release(); + + } else if (type_id_cf == CFNumberGetTypeID()) { + CFNumberRef number_cf = base::mac::CFCastStrict(property_cf); + NSNumber* number_ns = base::mac::CFToNSCast(number_cf); + switch (CFNumberGetType(number_cf)) { + case kCFNumberSInt8Type: + case kCFNumberSInt16Type: + case kCFNumberSInt32Type: + case kCFNumberSInt64Type: + case kCFNumberCharType: + case kCFNumberShortType: + case kCFNumberIntType: + case kCFNumberLongType: + case kCFNumberLongLongType: + case kCFNumberCFIndexType: + case kCFNumberNSIntegerType: { + data_launch = launch_data_new_integer([number_ns longLongValue]); + break; + } + + case kCFNumberFloat32Type: + case kCFNumberFloat64Type: + case kCFNumberFloatType: + case kCFNumberDoubleType: { + data_launch = launch_data_new_real([number_ns doubleValue]); + break; + } + + default: { return NULL; } + } + + } else if (type_id_cf == CFBooleanGetTypeID()) { + CFBooleanRef boolean_cf = + base::mac::CFCastStrict(property_cf); + data_launch = launch_data_new_bool(CFBooleanGetValue(boolean_cf)); + + } else if (type_id_cf == CFStringGetTypeID()) { + NSString* string_ns = base::mac::CFToNSCast( + base::mac::CFCastStrict(property_cf)); + + // -fileSystemRepresentation might be more correct than -UTF8String, because + // these strings can hold paths. The analogous function in launchctl, + // CF2launch_data (10.9.2 launchd-842.90.1/support/launchctl.c), uses UTF-8 + // instead of filesystem encoding, so do the same here. Note that there’s + // another occurrence of -UTF8String above, used for dictionary keys. + data_launch = launch_data_new_string([string_ns UTF8String]); + + } else if (type_id_cf == CFDataGetTypeID()) { + NSData* data_ns = + base::mac::CFToNSCast(base::mac::CFCastStrict(property_cf)); + data_launch = launch_data_new_opaque([data_ns bytes], [data_ns length]); + } else { + base::ScopedCFTypeRef type_name_cf( + CFCopyTypeIDDescription(type_id_cf)); + DLOG(ERROR) << "unable to convert CFTypeID " << type_id_cf << " (" + << base::SysCFStringRefToUTF8(type_name_cf) << ")"; + } + + return data_launch; +} + +} // namespace crashpad diff --git a/util/mac/launchd_test.mm b/util/mac/launchd_test.mm new file mode 100644 index 00000000..afa70032 --- /dev/null +++ b/util/mac/launchd_test.mm @@ -0,0 +1,292 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "util/mac/launchd.h" + +#import +#include +#include + +#include +#include + +#include "base/basictypes.h" +#include "base/mac/scoped_launch_data.h" +#include "gtest/gtest.h" +#include "util/stdlib/objc.h" + +namespace { + +using namespace crashpad; + +TEST(Launchd, CFPropertyToLaunchData_Integer) { + base::mac::ScopedLaunchData launch_data; + + NSNumber* integer_nses[] = { + @0, + @1, + @-1, + @0x7f, + @0x80, + @0xff, + @0x0100, + @0x7fff, + @0x8000, + @0xffff, + @0x00010000, + @0x7fffffff, + @0x80000000, + @0xffffffff, + @0x1000000000000000, + @0x7fffffffffffffff, + @0x8000000000000000, + @0xffffffffffffffff, + @0x0123456789abcdef, + @0xfedcba9876543210, + }; + + for (size_t index = 0; index < arraysize(integer_nses); ++index) { + NSNumber* integer_ns = integer_nses[index]; + launch_data.reset(CFPropertyToLaunchData(integer_ns)); + ASSERT_TRUE(launch_data.get()); + ASSERT_EQ(LAUNCH_DATA_INTEGER, launch_data_get_type(launch_data)); + EXPECT_EQ([integer_ns longLongValue], launch_data_get_integer(launch_data)) + << "index " << index; + } +} + +TEST(Launchd, CFPropertyToLaunchData_FloatingPoint) { + base::mac::ScopedLaunchData launch_data; + + @autoreleasepool { + NSNumber* double_nses[] = { + @0.0, + @1.0, + @-1.0, + [NSNumber numberWithFloat:std::numeric_limits::min()], + [NSNumber numberWithFloat:std::numeric_limits::max()], + [NSNumber numberWithFloat:std::numeric_limits::min()], + [NSNumber numberWithFloat:std::numeric_limits::max()], + @3.1415926535897932, + [NSNumber numberWithFloat:std::numeric_limits::infinity()], + [NSNumber numberWithFloat:std::numeric_limits::quiet_NaN()], + [NSNumber numberWithFloat:std::numeric_limits::signaling_NaN()], + }; + + for (size_t index = 0; index < arraysize(double_nses); ++index) { + NSNumber* double_ns = double_nses[index]; + launch_data.reset(CFPropertyToLaunchData(double_ns)); + ASSERT_TRUE(launch_data.get()); + ASSERT_EQ(LAUNCH_DATA_REAL, launch_data_get_type(launch_data)); + double expected_double_value = [double_ns doubleValue]; + double observed_double_value = launch_data_get_real(launch_data); + bool expected_is_nan = std::isnan(expected_double_value); + EXPECT_EQ(expected_is_nan, std::isnan(observed_double_value)); + if (!expected_is_nan) { + EXPECT_DOUBLE_EQ(expected_double_value, observed_double_value) + << "index " << index; + } + } + } +} + +TEST(Launchd, CFPropertyToLaunchData_Boolean) { + base::mac::ScopedLaunchData launch_data; + + NSNumber* bool_nses[] = { + @NO, + @YES, + }; + + for (size_t index = 0; index < arraysize(bool_nses); ++index) { + NSNumber* bool_ns = bool_nses[index]; + launch_data.reset(CFPropertyToLaunchData(bool_ns)); + ASSERT_TRUE(launch_data.get()); + ASSERT_EQ(LAUNCH_DATA_BOOL, launch_data_get_type(launch_data)); + if ([bool_ns boolValue]) { + EXPECT_TRUE(launch_data_get_bool(launch_data)); + } else { + EXPECT_FALSE(launch_data_get_bool(launch_data)); + } + } +} + +TEST(Launchd, CFPropertyToLaunchData_String) { + base::mac::ScopedLaunchData launch_data; + + @autoreleasepool { + NSString* string_nses[] = { + @"", + @"string", + @"Üñîçø∂é", + }; + + for (size_t index = 0; index < arraysize(string_nses); ++index) { + NSString* string_ns = string_nses[index]; + launch_data.reset(CFPropertyToLaunchData(string_ns)); + ASSERT_TRUE(launch_data.get()); + ASSERT_EQ(LAUNCH_DATA_STRING, launch_data_get_type(launch_data)); + EXPECT_STREQ([string_ns UTF8String], launch_data_get_string(launch_data)); + } + } +} + +TEST(Launchd, CFPropertyToLaunchData_Data) { + base::mac::ScopedLaunchData launch_data; + + @autoreleasepool { + const uint8_t data_c[] = { + 1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0, 9, 8, 7, 6, 5, 4, 3, 2}; + NSData* data_ns = [NSData dataWithBytes:data_c length:sizeof(data_c)]; + launch_data.reset(CFPropertyToLaunchData(data_ns)); + ASSERT_TRUE(launch_data.get()); + ASSERT_EQ(LAUNCH_DATA_OPAQUE, launch_data_get_type(launch_data)); + EXPECT_EQ(sizeof(data_c), launch_data_get_opaque_size(launch_data)); + EXPECT_EQ( + 0, memcmp(launch_data_get_opaque(launch_data), data_c, sizeof(data_c))); + } +} + +TEST(Launchd, CFPropertyToLaunchData_Dictionary) { + base::mac::ScopedLaunchData launch_data; + + NSDictionary* dictionary_ns = @{ + @"key" : @"value", + }; + + launch_data.reset(CFPropertyToLaunchData(dictionary_ns)); + ASSERT_TRUE(launch_data.get()); + ASSERT_EQ(LAUNCH_DATA_DICTIONARY, launch_data_get_type(launch_data)); + EXPECT_EQ([dictionary_ns count], launch_data_dict_get_count(launch_data)); + + launch_data_t launch_lookup_data = + launch_data_dict_lookup(launch_data, "key"); + ASSERT_TRUE(launch_lookup_data); + ASSERT_EQ(LAUNCH_DATA_STRING, launch_data_get_type(launch_lookup_data)); + EXPECT_STREQ("value", launch_data_get_string(launch_lookup_data)); +} + +TEST(Launchd, CFPropertyToLaunchData_Array) { + base::mac::ScopedLaunchData launch_data; + + NSArray* array_ns = @[ @"element_1", @"element_2", ]; + + launch_data.reset(CFPropertyToLaunchData(array_ns)); + ASSERT_TRUE(launch_data.get()); + ASSERT_EQ(LAUNCH_DATA_ARRAY, launch_data_get_type(launch_data)); + EXPECT_EQ([array_ns count], launch_data_array_get_count(launch_data)); + + launch_data_t launch_lookup_data = + launch_data_array_get_index(launch_data, 0); + ASSERT_TRUE(launch_lookup_data); + ASSERT_EQ(LAUNCH_DATA_STRING, launch_data_get_type(launch_lookup_data)); + EXPECT_STREQ("element_1", launch_data_get_string(launch_lookup_data)); + + launch_lookup_data = launch_data_array_get_index(launch_data, 1); + ASSERT_TRUE(launch_lookup_data); + ASSERT_EQ(LAUNCH_DATA_STRING, launch_data_get_type(launch_lookup_data)); + EXPECT_STREQ("element_2", launch_data_get_string(launch_lookup_data)); +} + +TEST(Launchd, CFPropertyToLaunchData_NSDate) { + // Test that NSDate conversions fail as advertised. There’s no type for + // storing date values in a launch_data_t. + + base::mac::ScopedLaunchData launch_data; + + @autoreleasepool { + NSDate* date = [NSDate date]; + launch_data.reset(CFPropertyToLaunchData(date)); + EXPECT_FALSE(launch_data); + + NSDictionary* date_dictionary = @{ + @"key" : @"value", + @"date" : date, + }; + launch_data.reset(CFPropertyToLaunchData(date_dictionary)); + EXPECT_FALSE(launch_data); + + NSArray* date_array = @[ @"string_1", date, @"string_2", ]; + launch_data.reset(CFPropertyToLaunchData(date_array)); + EXPECT_FALSE(launch_data); + } +} + +TEST(Launchd, CFPropertyToLaunchData_RealWorldJobDictionary) { + base::mac::ScopedLaunchData launch_data; + + NSDictionary* job_dictionary = @{ + @LAUNCH_JOBKEY_LABEL : @"com.example.job.rebooter", + @LAUNCH_JOBKEY_ONDEMAND : @YES, + @LAUNCH_JOBKEY_PROGRAMARGUMENTS : + @[ @"/bin/bash", @"-c", @"/sbin/reboot", ], + @LAUNCH_JOBKEY_MACHSERVICES : @{ + @"com.example.service.rebooter" : @YES, + }, + }; + + launch_data.reset(CFPropertyToLaunchData(job_dictionary)); + ASSERT_TRUE(launch_data.get()); + ASSERT_EQ(LAUNCH_DATA_DICTIONARY, launch_data_get_type(launch_data)); + EXPECT_EQ(4u, launch_data_dict_get_count(launch_data)); + + launch_data_t launch_lookup_data = + launch_data_dict_lookup(launch_data, LAUNCH_JOBKEY_LABEL); + ASSERT_TRUE(launch_lookup_data); + ASSERT_EQ(LAUNCH_DATA_STRING, launch_data_get_type(launch_lookup_data)); + EXPECT_STREQ("com.example.job.rebooter", + launch_data_get_string(launch_lookup_data)); + + launch_lookup_data = + launch_data_dict_lookup(launch_data, LAUNCH_JOBKEY_ONDEMAND); + ASSERT_TRUE(launch_lookup_data); + ASSERT_EQ(LAUNCH_DATA_BOOL, launch_data_get_type(launch_lookup_data)); + EXPECT_TRUE(launch_data_get_bool(launch_lookup_data)); + + launch_lookup_data = + launch_data_dict_lookup(launch_data, LAUNCH_JOBKEY_PROGRAMARGUMENTS); + ASSERT_TRUE(launch_lookup_data); + ASSERT_EQ(LAUNCH_DATA_ARRAY, launch_data_get_type(launch_lookup_data)); + EXPECT_EQ(3u, launch_data_array_get_count(launch_lookup_data)); + + launch_data_t launch_sublookup_data = + launch_data_array_get_index(launch_lookup_data, 0); + ASSERT_TRUE(launch_sublookup_data); + ASSERT_EQ(LAUNCH_DATA_STRING, launch_data_get_type(launch_sublookup_data)); + EXPECT_STREQ("/bin/bash", launch_data_get_string(launch_sublookup_data)); + + launch_sublookup_data = launch_data_array_get_index(launch_lookup_data, 1); + ASSERT_TRUE(launch_sublookup_data); + ASSERT_EQ(LAUNCH_DATA_STRING, launch_data_get_type(launch_sublookup_data)); + EXPECT_STREQ("-c", launch_data_get_string(launch_sublookup_data)); + + launch_sublookup_data = launch_data_array_get_index(launch_lookup_data, 2); + ASSERT_TRUE(launch_sublookup_data); + ASSERT_EQ(LAUNCH_DATA_STRING, launch_data_get_type(launch_sublookup_data)); + EXPECT_STREQ("/sbin/reboot", launch_data_get_string(launch_sublookup_data)); + + launch_lookup_data = + launch_data_dict_lookup(launch_data, LAUNCH_JOBKEY_MACHSERVICES); + ASSERT_TRUE(launch_lookup_data); + ASSERT_EQ(LAUNCH_DATA_DICTIONARY, launch_data_get_type(launch_lookup_data)); + EXPECT_EQ(1u, launch_data_dict_get_count(launch_lookup_data)); + + launch_sublookup_data = launch_data_dict_lookup( + launch_lookup_data, "com.example.service.rebooter"); + ASSERT_TRUE(launch_sublookup_data); + ASSERT_EQ(LAUNCH_DATA_BOOL, launch_data_get_type(launch_sublookup_data)); + EXPECT_TRUE(launch_data_get_bool(launch_sublookup_data)); +} + +} // namespace diff --git a/util/stdlib/objc.h b/util/stdlib/objc.h new file mode 100644 index 00000000..0b44bbab --- /dev/null +++ b/util/stdlib/objc.h @@ -0,0 +1,39 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef CRASHPAD_UTIL_STDLIB_OBJC_H_ +#define CRASHPAD_UTIL_STDLIB_OBJC_H_ + +#include +#include + +#if MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_8 + +// In order for the @NO and @YES literals to work, NO and YES must be defined as +// __objc_no and __objc_yes. See +// http://llvm.org/releases/3.1/tools/clang/docs/ObjectiveCLiterals.html . +// +// NO and YES are defined properly for this purpose in the 10.8 SDK, but not in +// earlier SDKs. Because this code is never expected to be compiled with a +// compiler that does not understand the modern forms of these boolean +// constants, but it may be built with an older SDK, replace the outdated SDK +// definitions unconditionally. +#undef NO +#undef YES +#define NO __objc_no +#define YES __objc_yes + +#endif + +#endif // CRASHPAD_UTIL_STDLIB_OBJC_H_ diff --git a/util/util.gyp b/util/util.gyp index 23acd937..422fb13c 100644 --- a/util/util.gyp +++ b/util/util.gyp @@ -32,11 +32,14 @@ 'file/file_writer.h', 'file/string_file_writer.cc', 'file/string_file_writer.h', + 'mac/launchd.h', + 'mac/launchd.mm', 'mach/task_memory.cc', 'mach/task_memory.h', 'misc/uuid.cc', 'misc/uuid.h', 'stdlib/cxx.h', + 'stdlib/objc.h', 'stdlib/strlcpy.cc', 'stdlib/strlcpy.h', ], @@ -75,6 +78,7 @@ 'sources': [ '../third_party/gtest/gtest/src/gtest_main.cc', 'file/string_file_writer_test.cc', + 'mac/launchd_test.mm', 'mach/task_memory_test.cc', 'misc/uuid_test.cc', 'stdlib/strlcpy_test.cc',