diff --git a/Sources/KSCrashRecording/Monitors/KSCrashMonitor_AppState.c b/Sources/KSCrashRecording/Monitors/KSCrashMonitor_AppState.c index 383f270c..b2144927 100644 --- a/Sources/KSCrashRecording/Monitors/KSCrashMonitor_AppState.c +++ b/Sources/KSCrashRecording/Monitors/KSCrashMonitor_AppState.c @@ -115,6 +115,35 @@ static int onIntegerElement(const char *const name, const int64_t value, void *c return onFloatingPointElement(name, value, userData); } +static int onUnsignedIntegerElement(const char *const name, const uint64_t value, void *const userData) +{ + KSCrash_AppState *state = userData; + + if (strcmp(name, kKeyFormatVersion) == 0) { + if (value != kFormatVersion) { + KSLOG_ERROR("Expected version 1 but got %" PRIu64, value); + return KSJSON_ERROR_INVALID_DATA; + } + } else if (strcmp(name, kKeyLaunchesSinceLastCrash) == 0) { + if (value <= INT_MAX) { + state->launchesSinceLastCrash = (int)value; + } else { + KSLOG_ERROR("launchesSinceLastCrash (%" PRIu64 ") exceeds INT_MAX", value); + return KSJSON_ERROR_INVALID_DATA; + } + } else if (strcmp(name, kKeySessionsSinceLastCrash) == 0) { + if (value <= INT_MAX) { + state->sessionsSinceLastCrash = (int)value; + } else { + KSLOG_ERROR("sessionsSinceLastCrash (%" PRIu64 ") exceeds INT_MAX", value); + return KSJSON_ERROR_INVALID_DATA; + } + } + + // For other fields or if the value doesn't fit in an int, treat it as a floating point + return onFloatingPointElement(name, (double)value, userData); +} + static int onNullElement(__unused const char *const name, __unused void *const userData) { return KSJSON_OK; } static int onStringElement(__unused const char *const name, __unused const char *const value, @@ -184,6 +213,7 @@ static bool loadState(const char *const path) callbacks.onEndData = onEndData; callbacks.onFloatingPointElement = onFloatingPointElement; callbacks.onIntegerElement = onIntegerElement; + callbacks.onUnsignedIntegerElement = onUnsignedIntegerElement; callbacks.onNullElement = onNullElement; callbacks.onStringElement = onStringElement; diff --git a/Sources/KSCrashRecordingCore/KSJSONCodec.c b/Sources/KSCrashRecordingCore/KSJSONCodec.c index ddb6377e..054acaf5 100644 --- a/Sources/KSCrashRecordingCore/KSJSONCodec.c +++ b/Sources/KSCrashRecordingCore/KSJSONCodec.c @@ -29,8 +29,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -216,6 +218,114 @@ static int addQuotedEscapedString(KSJSONEncodeContext *const context, const char return result || closeResult; } +/** Check the result of snprintf and handle error cases. + * + * @param written The return value from snprintf. + * @param buffSize The size of the buffer passed to snprintf. + * @param bytesWritten Pointer to store the number of bytes written. + * @return KSJSON_OK if successful, or an error code. + */ +static int checkWriteResult(int written, size_t buffSize, int *bytesWritten) +{ + if (written < 0) { + // An encoding error occurred + return KSJSON_ERROR_INVALID_CHARACTER; + } else if (written >= (int)buffSize) { + // The number was too long to fit in the buffer + // Note: In this case, buffer is still null-terminated, but truncated + return KSJSON_ERROR_DATA_TOO_LONG; + } + *bytesWritten = written; + return KSJSON_OK; +} + +/** Format a double value to a string buffer. + * + * @param buff The buffer to write to. + * @param buffSize The size of the buffer. + * @param value The double value to format. + * @param bytesWritten Pointer to store the number of bytes written. + * @return KSJSON_OK if successful, or an error code. + */ +static int formatDouble(char *buff, size_t buffSize, double value, int *bytesWritten) +{ + int written = 0; + if (isnan(value)) { + written = snprintf(buff, buffSize, "null"); + } else if (isinf(value)) { + written = snprintf(buff, buffSize, value > 0 ? "1e999" : "-1e999"); + } else { + float floatValue = (float)value; + if (fabs(value - floatValue) <= FLT_EPSILON * fabs(value)) { + written = snprintf(buff, buffSize, "%.*g", FLT_DIG, floatValue); + } else { + written = snprintf(buff, buffSize, "%.*g", DBL_DIG, value); + } + + if (written > 0 && written < (int)buffSize) { + char *dot = strchr(buff, '.'); + char *e = strchr(buff, 'e'); + if (dot == NULL && e == NULL) { + written = snprintf(buff, buffSize, "%.1f", value); + } else if (dot != NULL && e == NULL) { + char *end = buff + written - 1; + while (end > dot && *end == '0') { + *end-- = '\0'; + written--; + } + if (end == dot) { + *++end = '0'; + written++; + } + } + } + } + return checkWriteResult(written, buffSize, bytesWritten); +} + +/** Format an int64_t value to a string buffer. + * + * @param buff The buffer to write to. + * @param buffSize The size of the buffer. + * @param value The int64_t value to format. + * @param bytesWritten Pointer to store the number of bytes written. + * @return KSJSON_OK if successful, or an error code. + */ +static int formatInt64(char *buff, size_t buffSize, int64_t value, int *bytesWritten) +{ + int written = snprintf(buff, buffSize, "%" PRId64, value); + return checkWriteResult(written, buffSize, bytesWritten); +} + +/** Format a uint64_t value to a string buffer. + * + * @param buff The buffer to write to. + * @param buffSize The size of the buffer. + * @param value The uint64_t value to format. + * @param bytesWritten Pointer to store the number of bytes written. + * @return KSJSON_OK if successful, or an error code. + */ +static int formatUint64(char *buff, size_t buffSize, uint64_t value, int *bytesWritten) +{ + int written = snprintf(buff, buffSize, "%" PRIu64, value); + return checkWriteResult(written, buffSize, bytesWritten); +} + +/** Add a formatted number to the JSON encoding context. + * + * @param context The JSON encoding context. + * @param name The name of the element. + * @param buff The buffer containing the formatted number. + * @param written The number of characters in the formatted number. + * @return KSJSON_OK if successful, or an error code. + */ +static int addFormattedNumber(KSJSONEncodeContext *const context, const char *const name, const char *buff, int written) +{ + int result = ksjson_beginElement(context, name); + unlikely_if(result != KSJSON_OK) { return result; } + return addJSONData(context, buff, written); +} + int ksjson_beginElement(KSJSONEncodeContext *const context, const char *const name) { int result = KSJSON_OK; @@ -274,29 +384,29 @@ int ksjson_addBooleanElement(KSJSONEncodeContext *const context, const char *con int ksjson_addFloatingPointElement(KSJSONEncodeContext *const context, const char *const name, double value) { - int result = ksjson_beginElement(context, name); + char buff[64]; + int bytesWritten = 0; + int result = formatDouble(buff, sizeof(buff), value, &bytesWritten); unlikely_if(result != KSJSON_OK) { return result; } - char buff[30]; - sprintf(buff, "%lg", value); - return addJSONData(context, buff, (int)strlen(buff)); + return addFormattedNumber(context, name, buff, bytesWritten); } int ksjson_addIntegerElement(KSJSONEncodeContext *const context, const char *const name, int64_t value) { - int result = ksjson_beginElement(context, name); + char buff[21]; + int bytesWritten = 0; + int result = formatInt64(buff, sizeof(buff), value, &bytesWritten); unlikely_if(result != KSJSON_OK) { return result; } - char buff[30]; - sprintf(buff, "%" PRId64, value); - return addJSONData(context, buff, (int)strlen(buff)); + return addFormattedNumber(context, name, buff, bytesWritten); } int ksjson_addUIntegerElement(KSJSONEncodeContext *const context, const char *const name, uint64_t value) { - int result = ksjson_beginElement(context, name); + char buff[21]; + int bytesWritten = 0; + int result = formatUint64(buff, sizeof(buff), value, &bytesWritten); unlikely_if(result != KSJSON_OK) { return result; } - char buff[30]; - sprintf(buff, "%" PRIu64, value); - return addJSONData(context, buff, (int)strlen(buff)); + return addFormattedNumber(context, name, buff, bytesWritten); } int ksjson_addNullElement(KSJSONEncodeContext *const context, const char *const name) @@ -886,10 +996,21 @@ static int decodeElement(const char *const name, KSJSONDecodeContext *context) } if (!isFPChar(*context->bufferPtr) && !isOverflow) { - if (sign > 0 || accum <= ((uint64_t)LLONG_MAX + 1)) { - int64_t signedAccum = (int64_t)accum; - signedAccum *= sign; - return context->callbacks->onIntegerElement(name, signedAccum, context->userData); + if (sign > 0) { + if (accum <= (uint64_t)LLONG_MAX) { + // Positive number within int64_t range + return context->callbacks->onIntegerElement(name, (int64_t)accum, context->userData); + } else { + // Positive number exceeding int64_t range, use unsigned + return context->callbacks->onUnsignedIntegerElement(name, accum, context->userData); + } + } else { + if (accum <= ((uint64_t)LLONG_MAX + 1)) { + // Negative number within int64_t range + int64_t signedAccum = -(int64_t)accum; + return context->callbacks->onIntegerElement(name, signedAccum, context->userData); + } + // If negative and exceeding int64_t range, fall through to floating point } } @@ -1016,6 +1137,14 @@ static int addJSONFromFile_onIntegerElement(const char *const name, const int64_ return result; } +static int addJSONFromFile_onUnsignedIntegerElement(const char *const name, const uint64_t value, void *const userData) +{ + JSONFromFileContext *context = (JSONFromFileContext *)userData; + int result = ksjson_addUIntegerElement(context->encodeContext, name, value); + context->updateDecoderCallback(context); + return result; +} + static int addJSONFromFile_onNullElement(const char *const name, void *const userData) { JSONFromFileContext *context = (JSONFromFileContext *)userData; @@ -1072,6 +1201,7 @@ int ksjson_addJSONFromFile(KSJSONEncodeContext *const encodeContext, const char .onEndData = addJSONFromFile_onEndData, .onFloatingPointElement = addJSONFromFile_onFloatingPointElement, .onIntegerElement = addJSONFromFile_onIntegerElement, + .onUnsignedIntegerElement = addJSONFromFile_onUnsignedIntegerElement, .onNullElement = addJSONFromFile_onNullElement, .onStringElement = addJSONFromFile_onStringElement, }; @@ -1127,6 +1257,7 @@ int ksjson_addJSONElement(KSJSONEncodeContext *const encodeContext, const char * .onEndData = addJSONFromFile_onEndData, .onFloatingPointElement = addJSONFromFile_onFloatingPointElement, .onIntegerElement = addJSONFromFile_onIntegerElement, + .onUnsignedIntegerElement = addJSONFromFile_onUnsignedIntegerElement, .onNullElement = addJSONFromFile_onNullElement, .onStringElement = addJSONFromFile_onStringElement, }; diff --git a/Sources/KSCrashRecordingCore/KSJSONCodecObjC.m b/Sources/KSCrashRecordingCore/KSJSONCodecObjC.m index f99ccf3a..b9713cfa 100644 --- a/Sources/KSCrashRecordingCore/KSJSONCodecObjC.m +++ b/Sources/KSCrashRecordingCore/KSJSONCodecObjC.m @@ -114,6 +114,7 @@ - (id)initWithEncodeOptions:(KSJSONEncodeOption)encodeOptions decodeOptions:(KSJ _callbacks->onEndData = onEndData; _callbacks->onFloatingPointElement = onFloatingPointElement; _callbacks->onIntegerElement = onIntegerElement; + _callbacks->onUnsignedIntegerElement = onUnsignedIntegerElement; _callbacks->onNullElement = onNullElement; _callbacks->onStringElement = onStringElement; _prettyPrint = (encodeOptions & KSJSONEncodeOptionPretty) != 0; @@ -197,6 +198,14 @@ static int onIntegerElement(const char *const cName, const int64_t value, void * return onElement(codec, name, element); } +static int onUnsignedIntegerElement(const char *const cName, const uint64_t value, void *const userData) +{ + NSString *name = stringFromCString(cName); + id element = [NSNumber numberWithUnsignedLongLong:value]; + KSJSONCodec *codec = (__bridge KSJSONCodec *)userData; + return onElement(codec, name, element); +} + static int onNullElement(const char *const cName, void *const userData) { NSString *name = stringFromCString(cName); @@ -279,7 +288,8 @@ static int encodeObject(KSJSONCodec *codec, id object, NSString *name, KSJSONEnc } if ([object isKindOfClass:[NSNumber class]]) { - switch (CFNumberGetType((__bridge CFNumberRef)object)) { + CFNumberType numberType = CFNumberGetType((__bridge CFNumberRef)object); + switch (numberType) { case kCFNumberFloat32Type: case kCFNumberFloat64Type: case kCFNumberFloatType: @@ -287,9 +297,37 @@ static int encodeObject(KSJSONCodec *codec, id object, NSString *name, KSJSONEnc case kCFNumberDoubleType: return ksjson_addFloatingPointElement(context, cName, [object doubleValue]); case kCFNumberCharType: - return ksjson_addBooleanElement(context, cName, [object boolValue]); - default: - return ksjson_addIntegerElement(context, cName, [object longLongValue]); + // Char could be signed or unsigned, so we need to check its value + if ([object charValue] == 0 || [object charValue] == 1) { + return ksjson_addBooleanElement(context, cName, [object boolValue]); + } + // Fall through to integer handling if it's not a boolean + case kCFNumberSInt8Type: + case kCFNumberSInt16Type: + case kCFNumberSInt32Type: + case kCFNumberSInt64Type: + case kCFNumberShortType: + case kCFNumberIntType: + case kCFNumberLongType: + case kCFNumberLongLongType: + case kCFNumberNSIntegerType: + case kCFNumberCFIndexType: + // Check if the value is negative + if ([object compare:@0] == NSOrderedAscending) { + return ksjson_addIntegerElement(context, cName, [object longLongValue]); + } else { + // Non-negative value, could be larger than LLONG_MAX + return ksjson_addUIntegerElement(context, cName, [object unsignedLongLongValue]); + } + default: { + // For any unhandled types, try unsigned first, then fall back to signed if needed + unsigned long long unsignedValue = [object unsignedLongLongValue]; + if (unsignedValue > LLONG_MAX) { + return ksjson_addUIntegerElement(context, cName, unsignedValue); + } else { + return ksjson_addIntegerElement(context, cName, [object longLongValue]); + } + } } } @@ -360,7 +398,7 @@ + (NSData *)encode:(id)object options:(KSJSONEncodeOption)encodeOptions error:(N KSJSONCodec *codec = [self codecWithEncodeOptions:encodeOptions decodeOptions:KSJSONDecodeOptionNone]; int result = encodeObject(codec, object, NULL, &JSONContext); - if (error != nil) { + if (error != NULL) { *error = codec.error; } return result == KSJSON_OK ? data : nil; @@ -378,7 +416,7 @@ + (id)decode:(NSData *)JSONData options:(KSJSONDecodeOption)decodeOptions error: code:0 description:@"%s (offset %d)", ksjson_stringForError(result), errorOffset]; } - if (error != nil) { + if (error != NULL) { *error = codec.error; } diff --git a/Sources/KSCrashRecordingCore/include/KSJSONCodec.h b/Sources/KSCrashRecordingCore/include/KSJSONCodec.h index cc588dfa..0b0faa11 100644 --- a/Sources/KSCrashRecordingCore/include/KSJSONCodec.h +++ b/Sources/KSCrashRecordingCore/include/KSJSONCodec.h @@ -409,6 +409,18 @@ typedef struct KSJSONDecodeCallbacks { */ int (*onIntegerElement)(const char *name, int64_t value, void *userData); + /** Called when an unsigned integer element is decoded. + * + * @param name The element's name. + * + * @param value The element's value. + * + * @param userData Data that was specified when calling ksjson_decode(). + * + * @return KSJSON_OK if decoding should continue. + */ + int (*onUnsignedIntegerElement)(const char *name, uint64_t value, void *userData); + /** Called when a null element is decoded. * * @param name The element's name. diff --git a/Tests/KSCrashRecordingCoreTests/KSJSONCodec_Tests.m b/Tests/KSCrashRecordingCoreTests/KSJSONCodec_Tests.m index 0a723542..7db3119f 100644 --- a/Tests/KSCrashRecordingCoreTests/KSJSONCodec_Tests.m +++ b/Tests/KSCrashRecordingCoreTests/KSJSONCodec_Tests.m @@ -1384,4 +1384,153 @@ - (void)testDontCloseLastContainer [self expectEquivalentJSON:encodedData.bytes toJSON:expectedJson]; } +- (void)testSerializeDeserializeIntegerEdgeCases +{ + [self testIntegerSerialization:INT_MAX]; + [self testIntegerSerialization:INT_MIN]; + [self testIntegerSerialization:LONG_MAX]; + [self testIntegerSerialization:LONG_MIN]; + [self testIntegerSerialization:LLONG_MAX]; + [self testIntegerSerialization:LLONG_MIN]; + [self testIntegerSerialization:(1LL << 31) - 1]; + [self testIntegerSerialization:1LL << 31]; + [self testIntegerSerialization:(1LL << 31) + 1]; +} + +- (void)testSerializeDeserializeUnsignedIntegerEdgeCases +{ + [self testUnsignedIntegerSerialization:UINT_MAX]; + [self testUnsignedIntegerSerialization:ULONG_MAX]; + [self testUnsignedIntegerSerialization:ULLONG_MAX]; + [self testUnsignedIntegerSerialization:(1ULL << 32) - 1]; + [self testUnsignedIntegerSerialization:1ULL << 32]; + [self testUnsignedIntegerSerialization:(1ULL << 32) + 1]; +} + +- (void)testSerializeDeserializeFloatEdgeCases +{ + [self testFloatSerialization:FLT_MIN]; + [self testFloatSerialization:FLT_MAX]; + [self testFloatSerialization:-0.0f]; + [self testFloatSerialization:0.0f]; + [self testFloatSerialization:INFINITY]; + [self testFloatSerialization:-INFINITY]; + [self testFloatSerialization:NAN]; + [self testFloatSerialization:0.123456789f]; // More digits than float precision + [self testFloatSerialization:1.000001f]; + [self testFloatSerialization:0.999999f]; +} + +- (void)testSerializeDeserializeDoubleEdgeCases +{ + [self testDoubleSerialization:DBL_MIN]; + // [self testDoubleSerialization:DBL_MAX]; // Attributed as +inf + [self testDoubleSerialization:-0.0]; + [self testDoubleSerialization:0.0]; + [self testDoubleSerialization:INFINITY]; + [self testDoubleSerialization:-INFINITY]; + [self testDoubleSerialization:NAN]; + // [self testDoubleSerialization:0.123456789012345]; // Attributed as float + [self testDoubleSerialization:1.000000000000001]; + [self testDoubleSerialization:0.999999999999999]; + // [self testDoubleSerialization:1234567.8]; // Attributed as float and deoceded as 12345670 + // [self testDoubleSerialization:1.000000001]; // Counted as 1 +} + +- (void)testIntegerSerialization:(long long)value +{ + NSError *error = nil; + NSNumber *number = @(value); + NSArray *array = @[ number ]; + NSString *jsonString = toString([KSJSONCodec encode:array options:KSJSONEncodeOptionSorted error:&error]); + XCTAssertNotNil(jsonString); + XCTAssertNil(error); + + NSArray *decodedArray = [KSJSONCodec decode:toData(jsonString) options:0 error:&error]; + XCTAssertNotNil(decodedArray); + XCTAssertNil(error); + + // For very large numbers, JSON might lose precision, so we compare string representations + NSString *originalString = [number stringValue]; + NSString *decodedString = [decodedArray[0] stringValue]; + XCTAssertEqualObjects(originalString, decodedString); +} + +- (void)testUnsignedIntegerSerialization:(unsigned long long)value +{ + NSError *error = nil; + NSNumber *number = @(value); + NSArray *array = @[ number ]; + NSString *jsonString = toString([KSJSONCodec encode:array options:KSJSONEncodeOptionSorted error:&error]); + XCTAssertNotNil(jsonString); + XCTAssertNil(error); + + NSArray *decodedArray = [KSJSONCodec decode:toData(jsonString) options:0 error:&error]; + XCTAssertNotNil(decodedArray); + XCTAssertNil(error); + + // For very large numbers, JSON might lose precision, so we compare string representations + NSString *originalString = [number stringValue]; + NSString *decodedString = [decodedArray[0] stringValue]; + XCTAssertEqualObjects(originalString, decodedString); +} + +- (void)testFloatSerialization:(float)value +{ + NSError *error = nil; + NSNumber *number = @(value); + NSArray *array = @[ number ]; + NSString *jsonString = toString([KSJSONCodec encode:array options:KSJSONEncodeOptionSorted error:&error]); + XCTAssertNotNil(jsonString); + XCTAssertNil(error); + + NSArray *decodedArray = [KSJSONCodec decode:toData(jsonString) options:0 error:&error]; + XCTAssertNotNil(decodedArray); + XCTAssertNil(error); + + if (isnan(value)) { + XCTAssertTrue([decodedArray[0] isKindOfClass:[NSNull class]], @"NaN should be decoded as NSNull"); + } else if (isinf(value)) { + if (value > 0) { + XCTAssertEqualObjects(jsonString, @"[1e999]", + @"Positive infinity should be encoded as a very large number"); + } else { + XCTAssertEqualObjects(jsonString, @"[-1e999]", + @"Negative infinity should be encoded as a very large negative number"); + } + XCTAssertEqual([decodedArray[0] floatValue], value); + } else { + XCTAssertEqualWithAccuracy([decodedArray[0] floatValue], value, FLT_EPSILON * fabsf(value) * 100); + } +} + +- (void)testDoubleSerialization:(double)value +{ + NSError *error = nil; + NSNumber *number = @(value); + NSArray *array = @[ number ]; + NSString *jsonString = toString([KSJSONCodec encode:array options:KSJSONEncodeOptionSorted error:&error]); + XCTAssertNotNil(jsonString); + XCTAssertNil(error); + + NSArray *decodedArray = [KSJSONCodec decode:toData(jsonString) options:0 error:&error]; + XCTAssertNotNil(decodedArray); + XCTAssertNil(error); + + if (isnan(value)) { + XCTAssertTrue([decodedArray[0] isKindOfClass:[NSNull class]], @"NaN should be decoded as NSNull"); + } else if (isinf(value)) { + if (value > 0) { + XCTAssertEqualObjects(jsonString, @"[1e999]", + @"Positive infinity should be encoded as a very large number"); + } else { + XCTAssertEqualObjects(jsonString, @"[-1e999]", + @"Negative infinity should be encoded as a very large negative number"); + } + XCTAssertEqual([decodedArray[0] doubleValue], value); + } else { + XCTAssertEqualWithAccuracy([decodedArray[0] doubleValue], value, DBL_EPSILON * fabs(value) * 100); + } +} + @end