Skip to content

Commit

Permalink
OSC-10 (#12)
Browse files Browse the repository at this point in the history
- During parsing of both the address pattern and type tag string, checks are made so that we don’t parse any invalid characters.
- Fixed crash with not closed brackets and curly braces e.g. "/core/[o-" and "/core/{osc"
- Fixed crash with asterisk wildcard e.g. "/core/*sc"
  • Loading branch information
sammysmallman committed Oct 2, 2023
1 parent 95a9371 commit 887328e
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 78 deletions.
2 changes: 1 addition & 1 deletion Sources/CoreOSC/CoreOSC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import Foundation
public enum CoreOSC {

/// This package's semantic version number, mirrored also in git history as a `git tag`.
public static let version: String = "1.3.0"
public static let version: String = "1.3.1"

/// The license agreement this repository is licensed under.
public static let license: String = {
Expand Down
2 changes: 1 addition & 1 deletion Sources/CoreOSC/LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright © 2022 Sam Smallman. https://github.com/SammySmallman
Copyright © 2023 Sam Smallman. https://github.com/SammySmallman

This file is part of CoreOSC

Expand Down
45 changes: 16 additions & 29 deletions Sources/CoreOSC/OSCMatch.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ public enum OSCMatch {
addressCharacterOffset: &addressCharacterOffset)
if match == false {
if patternCharacterOffset != addressPattern.endIndex &&
addressPattern.index(after: patternCharacterOffset) == addressPattern.endIndex &&
addressPattern[patternCharacterOffset] == "]" {
addressPattern.index(after: patternCharacterOffset) == addressPattern.endIndex &&
addressPattern[patternCharacterOffset] == "]" {
return OSCPatternMatch(match: .unmatched,
patternCharactersMatched: addressPattern.distance(from: addressPattern.startIndex,
to: addressPattern.endIndex),
Expand Down Expand Up @@ -118,7 +118,6 @@ public enum OSCMatch {
patternCharactersMatched: 0,
addressCharactersMatched: 0)
}

return OSCPatternMatch(match: matching,
patternCharactersMatched:
addressPattern.distance(from: addressPattern.startIndex,
Expand All @@ -141,37 +140,19 @@ public enum OSCMatch {
patternCharacterOffset: inout String.Index,
address: String,
addressCharacterOffset: inout String.Index) -> Bool {
var numberOfAsterisks = 0
if addressCharacterOffset == address.endIndex { return false }
// Move address index up to next "/"
while addressCharacterOffset != address.endIndex &&
address[addressCharacterOffset] != "/" {
addressCharacterOffset = address.index(after: addressCharacterOffset)
}
// Move pattern index up to next "/"
while patternCharacterOffset != pattern.endIndex &&
pattern[patternCharacterOffset] != "/" {
if pattern[patternCharacterOffset] == "*" {
numberOfAsterisks += 1
}
patternCharacterOffset = pattern.index(after: patternCharacterOffset)
}

patternCharacterOffset = pattern.index(before: patternCharacterOffset)
addressCharacterOffset = address.index(before: addressCharacterOffset)
switch numberOfAsterisks {
case 1:
var casePatternCharacterOffset: String.Index = patternCharacterOffset
var caseAddressCharacterOffsetStart: String.Index = addressCharacterOffset
while pattern[casePatternCharacterOffset] != "*" {
if matchCharacters(pattern: pattern,
patternCharacterOffset: &casePatternCharacterOffset,
address: address,
addressCharacterOffset: &caseAddressCharacterOffsetStart) == false {
return false
}
}
break
default: return false
}
// TODO: Match patterns backwards if the last character before the "/" is not a "*"
return true
}

Expand Down Expand Up @@ -224,17 +205,19 @@ public enum OSCMatch {
while patternCharacterOffset != pattern.endIndex &&
pattern[patternCharacterOffset] != "]" {
if pattern[pattern.index(after: patternCharacterOffset)] == "-" {
if address[addressCharacterOffset].asciiValue! >= pattern[patternCharacterOffset].asciiValue! &&
address[addressCharacterOffset].asciiValue! <= pattern[pattern.index(patternCharacterOffset,
offsetBy: 2)].asciiValue! {
if address[addressCharacterOffset].asciiValue! >= pattern[patternCharacterOffset].asciiValue!,
let index = pattern.index(patternCharacterOffset, offsetBy: 2, limitedBy: pattern.index(before: pattern.endIndex)),
address[addressCharacterOffset].asciiValue! <= pattern[index].asciiValue! {
matched = val
while patternCharacterOffset != pattern.endIndex &&
pattern[patternCharacterOffset] != "]" {
patternCharacterOffset = pattern.index(after: patternCharacterOffset)
}
break
} else if let index = pattern.index(patternCharacterOffset, offsetBy: 3, limitedBy: pattern.index(before: pattern.endIndex)) {
patternCharacterOffset = index
} else {
patternCharacterOffset = pattern.index(patternCharacterOffset, offsetBy: 3)
return false
}
} else {
if pattern[patternCharacterOffset] == address[addressCharacterOffset] {
Expand Down Expand Up @@ -283,8 +266,12 @@ public enum OSCMatch {
pattern[offset] != "/" {
offset = pattern.index(after: offset)
}
patternCharacterOffset = offset
if pattern.endIndex == pattern.index(patternCharacterOffset, offsetBy: distance) || pattern[offset] != "}" {
patternCharacterOffset = startIndex
return false
}
addressCharacterOffset = address.index(addressCharacterOffset, offsetBy: distance - 1)
patternCharacterOffset = offset
return true
} else {
offset = pattern.index(after: offset)
Expand Down
76 changes: 56 additions & 20 deletions Sources/CoreOSC/OSCParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public enum OSCParser {
private static func process(bundle data: Data) throws -> OSCPacket {
return try parseOSCBundle(with: data)
}

/// Parse `OSCMessage` data.
/// - Parameters:
/// - data: The `Data` to parse.
Expand All @@ -68,12 +68,27 @@ public enum OSCParser {
/// - Returns: An `OSCMessage`.
private static func parseOSCMessage(with data: Data, startIndex: inout Int) throws -> OSCMessage {
guard let addressPattern = parse(string: data,
startIndex: &startIndex) else {
startIndex: &startIndex,
characters: .invalid(bytes: [
0x20, // Space - ' '
0x23 // Hash - #
])) else {
throw OSCParserError.cantParseAddressPattern
}

guard var typeTagString = parse(string: data,
startIndex: &startIndex) else {
startIndex: &startIndex,
characters: .valid(bytes: [
0x2C, // Comma - ,
0x73, // s
0x69, // i
0x66, // f
0x62, // b
0x74, // t
0x54, // T
0x46, // F
0x4E, // N
0x49 // I
])) else {
throw OSCParserError.cantParseTypeTagString
}

Expand Down Expand Up @@ -124,7 +139,8 @@ public enum OSCParser {
case .oscTypeTagImpulse:
arguments.append(OSCArgument.impulse)
default:
continue
// We shouldn't ever get here as we've checked the type tag string for invalid characters.
throw OSCParserError.cantParseTypeTagString
}
}
}
Expand Down Expand Up @@ -213,28 +229,48 @@ public enum OSCParser {
} while buffer < size
return elements
}


/// ASCII Characters that are either valid or invalid when parsing an OSC string.
enum OSCCharacters {
/// A collection of valid ASCII characters.
case valid(bytes: Set<UInt8>)
/// A collection of invalid ASCII characters.
case invalid(bytes: Set<UInt8>)
}

/// Parse OSC string data.
/// - Parameters:
/// - data: The `Data` to parse.
/// - startIndex: The index of where to start parsing from in the `Data`.
/// - Returns: A `String` or nil if an OSC string could not be parsed with the given data.
private static func parse(string data: Data, startIndex: inout Int) -> String? {
private static func parse(string data: Data, startIndex: inout Int, characters: OSCCharacters? = nil) -> String? {
// Read the data from the start index until you hit a zero, the part before will be the string data.
for (index, byte) in data[startIndex...].enumerated() where byte == 0x0 {
guard let result = String(data: data[startIndex..<(startIndex + index)],
encoding: .utf8) else { return nil }
// An OSC String is a sequence of non-null ASCII characters followed by a null,
// followed by 0-3 additional null characters to make the total number
// of bits a multiple of 32 Bits, 4 Bytes.
let bytesRead = startIndex + index + 1 // Include the Null bytes we found.
if bytesRead.isMultiple(of: 4) {
startIndex = bytesRead
} else {
let number = (Double(bytesRead) / 4.0).rounded(.up)
startIndex = Int(4.0 * number)
for (index, byte) in data[startIndex...].enumerated() {
if byte == 0x0 {
guard let result = String(data: data[startIndex..<(startIndex + index)],
encoding: .utf8) else { return nil }
// An OSC String is a sequence of non-null ASCII characters followed by a null,
// followed by 0-3 additional null characters to make the total number
// of bits a multiple of 32 Bits, 4 Bytes.
let bytesRead = startIndex + index + 1 // Include the Null bytes we found.
if bytesRead.isMultiple(of: 4) {
startIndex = bytesRead
} else {
let number = (Double(bytesRead) / 4.0).rounded(.up)
startIndex = Int(4.0 * number)
}
return result

} else if let characters = characters {
switch characters {
case .valid(let bytes):
// ,sifbtTFNI are the only characters valid in an OSC Type Tag string.
if !bytes.contains(byte) { return nil }
case .invalid(let bytes):
// Space and Hash characters are invalid in an OSC Address Pattern.
if bytes.contains(byte) { return nil }
}
}
return result
}
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/CoreOSC/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* OSC Address Error */

/* OSC Version */
"OSC_VERSION" = "1.3.0";
"OSC_VERSION" = "1.3.1";

/* OSC Address Error: Invalid Address */
"OSC_ADDRESS_ERROR_INVALID_ADDRESS" = "Invalid address";
Expand Down
4 changes: 2 additions & 2 deletions Tests/CoreOSCTests/CoreOSCTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ import XCTest
class CoreOSCTests: XCTestCase {

func testVersion() {
XCTAssertEqual(CoreOSC.version, "1.3.0")
XCTAssertEqual(CoreOSC.version, "1.3.1")
XCTAssertEqual(NSLocalizedString("OSC_VERSION", bundle: .module, comment: "OSC Version"), CoreOSC.version)
}

func testLicense() {
let license = CoreOSC.license
XCTAssertTrue(license.hasPrefix("Copyright © 2022 Sam Smallman. https://github.com/SammySmallman"))
XCTAssertTrue(license.hasPrefix("Copyright © 2023 Sam Smallman. https://github.com/SammySmallman"))
XCTAssertTrue(license.hasSuffix("<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"))
}

Expand Down
64 changes: 59 additions & 5 deletions Tests/CoreOSCTests/OSCMatchTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,18 @@ final class OSCMatchTests: XCTestCase {
OSCPatternMatch(match: .fullMatch,
patternCharactersMatched: "/*/*/*".count,
addressCharactersMatched: "/abc/def/hij".count))

XCTAssertEqual(OSCMatch.match(addressPattern: "/abc/*",
address: "/abc/def"),
OSCPatternMatch(match: .fullMatch,
patternCharactersMatched: "/abc/*".count,
addressCharactersMatched: "/abc/def".count))

XCTAssertEqual(OSCMatch.match(addressPattern: "/a/*cd",
address: "/a/bcd"),
OSCPatternMatch(match: .fullMatch,
patternCharactersMatched: "/a/*cd".count,
addressCharactersMatched: "/a/bcd".count))
}

func testAsteriskPartialAddressMatch() {
Expand All @@ -85,15 +97,23 @@ final class OSCMatchTests: XCTestCase {
patternCharactersMatched: "/a/b/c".count,
addressCharactersMatched: "/a/b/c".count))
}

func testAsteriskUnmatched() {
XCTAssertEqual(OSCMatch.match(addressPattern: "/*/abc",
address: "/abc/def"),
OSCPatternMatch(match: .unmatched,
patternCharactersMatched: "/*/".count,
addressCharactersMatched: "/abc/".count))

// TODO: Match patterns backwards if the last character before the "/" is not a "*"
// OSCMatch.swift:156
// XCTAssertEqual(OSCMatch.match(addressPattern: "/a/*cd",
// address: "/a/bef"),
// OSCPatternMatch(match: .unmatched,
// patternCharactersMatched: "/a/*".count,
// addressCharactersMatched: "/a/b".count))
}

// MARK: - Question Mark Wildcard OSC Address Pattern Tests

func testQuestionMarkFullMatch() {
Expand Down Expand Up @@ -257,7 +277,21 @@ final class OSCMatchTests: XCTestCase {
patternCharactersMatched: "/abc/[!d-f]".count,
addressCharactersMatched: "/abc/".count))
}


func testInvalidSquareBracketsNotClosed() {
XCTAssertEqual(OSCMatch.match(addressPattern: "/abc/[d-",
address: "/abc/d"),
OSCPatternMatch(match: .unmatched,
patternCharactersMatched: "/abc/[".count,
addressCharactersMatched: "/abc/".count))

XCTAssertEqual(OSCMatch.match(addressPattern: "/abc/[!d-",
address: "/abc/d"),
OSCPatternMatch(match: .unmatched,
patternCharactersMatched: "/abc/[!".count,
addressCharactersMatched: "/abc/".count))
}

// MARK: - Curly Braces Wildcard OSC Address Pattern Tests

func testCurlyBracesFullMatch() {
Expand All @@ -284,14 +318,34 @@ final class OSCMatchTests: XCTestCase {
addressCharactersMatched: "/abc".count))
}

func testCurlyBraceUnmatched() {
func testCurlyBracesUnmatched() {
XCTAssertEqual(OSCMatch.match(addressPattern: "/{abc,def}/{ghi,jkl}",
address: "/abc/mno"),
OSCPatternMatch(match: .unmatched,
patternCharactersMatched: "/{abc,def}/".count,
addressCharactersMatched: "/abc/".count))
}


func testInvalidCurlyBracesNotClosed() {
XCTAssertEqual(OSCMatch.match(addressPattern: "/abc/{d",
address: "/abc/d"),
OSCPatternMatch(match: .unmatched,
patternCharactersMatched: "/abc/".count,
addressCharactersMatched: "/abc/".count))

XCTAssertEqual(OSCMatch.match(addressPattern: "/abc/{d/ghi",
address: "/abc/d/g"),
OSCPatternMatch(match: .unmatched,
patternCharactersMatched: "/abc/".count,
addressCharactersMatched: "/abc/".count))

XCTAssertEqual(OSCMatch.match(addressPattern: "/{a}/{d/g",
address: "/a/d/g"),
OSCPatternMatch(match: .unmatched,
patternCharactersMatched: "/{a}/".count,
addressCharactersMatched: "/a/".count))
}

// MARK: - All Wildcards OSC Address Pattern Tests

func testAllWildcardsFullMatch() {
Expand Down
Loading

0 comments on commit 887328e

Please sign in to comment.