Skip to content

Commit

Permalink
Update NSString+F53OSCString to count bytes of composed character seq…
Browse files Browse the repository at this point in the history
…uences

* Add F53OSC_NSStringTests.m

Fixes #36

Co-authored-by: Brent Lord <[email protected]>
  • Loading branch information
richardwilliamson and balord authored Feb 10, 2022
1 parent 16f978b commit 57c517a
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 11 deletions.
4 changes: 4 additions & 0 deletions F53OSC.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
3DA5A9AB242FC4AB0068AF88 /* GCDAsyncSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DA5A9A1242FC4AB0068AF88 /* GCDAsyncSocket.h */; settings = {ATTRIBUTES = (Public, ); }; };
3DA5A9AC242FC4AB0068AF88 /* GCDAsyncSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DA5A9A1242FC4AB0068AF88 /* GCDAsyncSocket.h */; settings = {ATTRIBUTES = (Public, ); }; };
3DA5A9AD242FC4AB0068AF88 /* GCDAsyncSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DA5A9A1242FC4AB0068AF88 /* GCDAsyncSocket.h */; settings = {ATTRIBUTES = (Public, ); }; };
3DD52F4427B2B80400F1DD6B /* F53OSC_NSStringTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DD52F4327B2B80400F1DD6B /* F53OSC_NSStringTests.m */; };
66AFE43C1B79485100985C54 /* ActivityChartView.m in Sources */ = {isa = PBXBuildFile; fileRef = 66AFE43B1B79485100985C54 /* ActivityChartView.m */; };
66EE17591B729EA0008B6743 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 66EE17581B729EA0008B6743 /* AppDelegate.m */; };
66EE175B1B729EA0008B6743 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 66EE175A1B729EA0008B6743 /* main.m */; };
Expand Down Expand Up @@ -218,6 +219,7 @@
3DA5A9A0242FC4AB0068AF88 /* GCDAsyncUdpSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncUdpSocket.h; sourceTree = "<group>"; };
3DA5A9A1242FC4AB0068AF88 /* GCDAsyncSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncSocket.h; sourceTree = "<group>"; };
3DA5A9B02432674F0068AF88 /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
3DD52F4327B2B80400F1DD6B /* F53OSC_NSStringTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = F53OSC_NSStringTests.m; sourceTree = "<group>"; };
66AFE43A1B79485100985C54 /* ActivityChartView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ActivityChartView.h; sourceTree = "<group>"; };
66AFE43B1B79485100985C54 /* ActivityChartView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ActivityChartView.m; sourceTree = "<group>"; };
66EE17521B729EA0008B6743 /* F53OSC Monitor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "F53OSC Monitor.app"; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -284,6 +286,7 @@
isa = PBXGroup;
children = (
3D1E07FF242A7E1000655E76 /* F53OSC_NSNumberTests.m */,
3DD52F4327B2B80400F1DD6B /* F53OSC_NSStringTests.m */,
3D1E07FE242A7E1000655E76 /* F53OSCMessageTests.m */,
3D1E07FD242A7E1000655E76 /* F53OSCServerTests.m */,
);
Expand Down Expand Up @@ -768,6 +771,7 @@
files = (
3D1E08AF242A847E00655E76 /* F53OSC_NSNumberTests.m in Sources */,
3D1E08AE242A847C00655E76 /* F53OSCMessageTests.m in Sources */,
3DD52F4427B2B80400F1DD6B /* F53OSC_NSStringTests.m in Sources */,
3D1E08AD242A847A00655E76 /* F53OSCServerTests.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
24 changes: 13 additions & 11 deletions Sources/F53OSC/NSString+F53OSCString.m
Original file line number Diff line number Diff line change
Expand Up @@ -70,26 +70,28 @@ + (nullable NSString *) stringWithOSCStringBytes:(const char *)buf maxLength:(NS
return nil;
}

NSUInteger bytesRead = 0;
for ( NSUInteger index = 0; index < maxLength; index++ )
{
if ( buf[index] == 0 )
goto valid; // found a NULL character within the buffer
{
// found a NULL character within the buffer
bytesRead = index + 1; // include length of null terminator character
break;
}
}
if (bytesRead == 0)
{
// Buffer wasn't null terminated, so it's not a valid OSC string.
if ( outBytesRead != NULL )
*outBytesRead = 0;
return nil;
}

// Buffer wasn't null terminated, so it's not a valid OSC string.
if ( outBytesRead != NULL )
*outBytesRead = 0;
return nil;

valid:;

NSString *result = [NSString stringWithUTF8String:buf];

if ( outBytesRead != NULL )
{
NSUInteger bytesRead = result.length + 1; // include length of null terminator character
*outBytesRead = 4 * ceil( bytesRead / 4.0 ); // round up to a multiple of 32 bits
}

return result;
}
Expand Down
182 changes: 182 additions & 0 deletions Tests/F53OSCTests/F53OSC_NSStringTests.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
//
// F53OSC_NSStringTests.m
// F53OSC
//
// Created by Brent Lord on 2/8/22.
// Copyright (c) 2022 Figure 53. All rights reserved.
//

#if !__has_feature(objc_arc)
#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif

#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#import "F53OSCMessage.h"
#import "F53OSCParser.h"
#import "NSString+F53OSCString.h"


NS_ASSUME_NONNULL_BEGIN

@interface F53OSC_NSStringTests : XCTestCase
@end


@implementation F53OSC_NSStringTests

- (void) setUp
{
[super setUp];
// Put setup code here. This method is called before the invocation of each test method in the class.
}

- (void) tearDown
{
// Put teardown code here. This method is called after the invocation of each test method in the class.
[super tearDown];
}

NS_INLINE NSUInteger fourBytePaddedInteger(NSUInteger value)
{
return 4 * (ceil((value + 1) / 4.0));
}

- (void)testThat_oscStringDataLengthsAreCorrect
{
// given
// OSC strings are null-terminated and encoded as data in multiples of 4 bytes.
// If the data is already a multiple of 4 bytes, it gets an additional four null bytes appended.
NSDictionary<NSString *, NSNumber *> *testStrings = @{
@"" : @4, // 0 bytes + 4 null
@"a" : @4, // 1 bytes + 3 null
@"ab" : @4, // 2 bytes + 3 null
@"abc" : @4, // 3 bytes + 1 null

@"abcd" : @8, // 4 bytes + 4 null, etc.
@"abcde" : @8,
@"abcdef" : @8,
@"abcdefg" : @8,

@"abcdefgh" : @12,
@"abcdefghi" : @12,
@"abcdefghij" : @12,
@"abcdefghijk" : @12,

// two-byte composed characters encoded as NSUTF8StringEncoding
@"å" : @4, // 2 bytes + 2 null
@"åb" : @4, // 3 bytes + 1 null
@"åbc" : @8, // 4 bytes + 4 null
@"åbcd" : @8, // 5 bytes + 3 null
@"åbcdé" : @8, // 7 bytes + 1 null
@"åéîøü" : @12, // 10 bytes + 2 null

// three-byte composed characters encoded as NSUTF8StringEncoding
@"" : @4, // 3 bytes + 1 null (U+90F5)
@"郵政" : @8, // 6 bytes + 2 null
@"MAIL ROOM 郵政" : @20, // 16 bytes + 4 null
};

NSData *oscStringData;
NSString *decodedString;
for (NSString *testString in testStrings)
{
// ENCODE
// when
oscStringData = [testString oscStringData];
NSUInteger expectedDataLength = [testStrings[testString] unsignedIntegerValue];

// then
XCTAssertNotNil(oscStringData, "%@", testString);
XCTAssertEqual(oscStringData.length, expectedDataLength, "%@", testString);

// DECODE
// when
NSUInteger bytesRead = 0;
decodedString = [NSString stringWithOSCStringBytes:oscStringData.bytes maxLength:oscStringData.length bytesRead:&bytesRead];

// then
XCTAssertNotNil(decodedString, "%@", testString);
XCTAssertGreaterThanOrEqual(bytesRead, 4, "%@", testString); // empty string encodes as a minimum of 4 null bytes
XCTAssertEqual(bytesRead % 4, 0, "%@", testString); // multiple of 4 bytes

XCTAssertEqualObjects(testString, decodedString, "%@", testString);
XCTAssertEqual([testString compare:decodedString options:NSDiacriticInsensitiveSearch], NSOrderedSame, "%@", testString);
XCTAssertEqualObjects(testString.decomposedStringWithCanonicalMapping, decodedString.decomposedStringWithCanonicalMapping);

XCTAssertEqual(testString.length, decodedString.length, "%@", testString);
XCTAssertEqual([testString lengthOfBytesUsingEncoding:NSUTF8StringEncoding], [decodedString lengthOfBytesUsingEncoding:NSUTF8StringEncoding], "%@", testString);

XCTAssertEqual(bytesRead, expectedDataLength, "%@", testString);

XCTAssertEqual(fourBytePaddedInteger([testString lengthOfBytesUsingEncoding:NSUTF8StringEncoding]), bytesRead, "%@", testString);
XCTAssertEqual(fourBytePaddedInteger([decodedString lengthOfBytesUsingEncoding:NSUTF8StringEncoding]), bytesRead, "%@", testString);

NSString *escapedTestString = [testString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLPathAllowedCharacterSet]];
if ([testString isEqual:escapedTestString] == NO)
{
// NSString `-length` counts composed characters as 1, while `-lengthOfBytesUsingEncoding` & `bytesRead` counts the decomposed character length.
XCTAssertLessThan(testString.length, [testString lengthOfBytesUsingEncoding:NSUTF8StringEncoding], "%@", testString);
XCTAssertLessThan(decodedString.length, [decodedString lengthOfBytesUsingEncoding:NSUTF8StringEncoding], "%@", testString);
XCTAssertLessThanOrEqual(fourBytePaddedInteger(testString.length), bytesRead, "%@", testString);
XCTAssertLessThanOrEqual(fourBytePaddedInteger(decodedString.length), bytesRead, "%@", testString);
}
else
{
XCTAssertEqual(testString.length, [testString lengthOfBytesUsingEncoding:NSUTF8StringEncoding], "%@", testString);
XCTAssertEqual(decodedString.length, [decodedString lengthOfBytesUsingEncoding:NSUTF8StringEncoding], "%@", testString);
XCTAssertEqual(fourBytePaddedInteger(testString.length), bytesRead, "%@", testString);
XCTAssertEqual(fourBytePaddedInteger(decodedString.length), bytesRead, "%@", testString);
}
}
}

- (void)testThat_oscStringArgumentsCanBeParsed
{
// given
NSArray<NSArray<NSString *> *> *allTestArgs = @[
@[@"a"],
@[@"a", @"ab"],
@[@"a", @"ab", @"abc"],
@[@"a", @"ab", @"abc", @"abcd"],

// two-byte composed characters
@[@"å", @"åb"],
@[@"a", @"åb", @"abc"],
@[@"å", @"åb", @"abc", @"abcd"],
@[@"å", @"ab", @"åbc", @"åbcd", @"åbcdé"],
@[@"å", @"ab", @"åbc", @"åbcd", @"åbcdé", @"åbcdéf"],

// three-byte composed characters encoded as NSUTF8StringEncoding
@[@""],
@[@"", @""],
@[@"123", @"", @"456"],
@[@"MAIL ROOM 郵政", @"123"],
@[@"123", @"MAIL ROOM 郵政", @"456"],
];

F53OSCMessage *testMessage;
for (NSArray<NSString *> *testArgs in allTestArgs)
{
testMessage = [F53OSCMessage messageWithAddressPattern:@"/some/method" arguments:testArgs];
XCTAssertNotNil(testMessage, "%@", testArgs);
XCTAssertEqualObjects(testMessage.arguments, testArgs, "%@", testArgs);
XCTAssertNotNil(testMessage.packetData, "%@", testArgs);

// when
NSData *packetData = testMessage.packetData; // encodes strings for OSC
F53OSCMessage *parsedMessage = [F53OSCParser parseOscMessageData:packetData]; // decodes data

// then
XCTAssertNotNil(parsedMessage, "%@", testArgs);
XCTAssertEqualObjects(parsedMessage.arguments, testArgs, "%@", testArgs);
XCTAssertEqualObjects(testMessage, parsedMessage, "%@", testArgs);
XCTAssertEqualObjects(testMessage.addressPattern, parsedMessage.addressPattern, "%@", testArgs);
XCTAssertEqual(testMessage.arguments.count, parsedMessage.arguments.count, "%@", testArgs);
XCTAssertTrue([testMessage.arguments isEqualToArray:parsedMessage.arguments], "%@", testArgs);
}
}

@end

NS_ASSUME_NONNULL_END

0 comments on commit 57c517a

Please sign in to comment.