Skip to content

Commit 2832e79

Browse files
authored
perf: optimized validString in CommentedString (#1067)
* Optimized CommentedString * updated minimum macos version in Package.swift * remove macos version check and make variable names more meaningful * fix lint issues
1 parent 01fbdd7 commit 2832e79

File tree

3 files changed

+186
-34
lines changed

3 files changed

+186
-34
lines changed

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import PackageDescription
44

55
let package = Package(
66
name: "XcodeProj",
7+
platforms: [.macOS(.v11)],
78
products: [
89
.library(name: "XcodeProj", targets: ["XcodeProj"]),
910
],
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
extension Collection where Element: BinaryInteger, Index == Int {
2+
@inlinable
3+
@inline(__always)
4+
func containsCString<T: BidirectionalCollection>(_ cString: T) -> Bool where T.Element: BinaryInteger, T.Index == Int {
5+
guard !cString.isEmpty else { return true }
6+
7+
// Drop null terminator if present
8+
let subarrayCount = cString.last == 0
9+
? cString.count - 1
10+
: cString.count
11+
12+
guard subarrayCount <= count else { return false }
13+
14+
let lastSubarrayStartingPos = count - subarrayCount
15+
var i = 0
16+
while i <= lastSubarrayStartingPos {
17+
var match = true
18+
var j = 0
19+
while j < subarrayCount {
20+
if self[i + j] != cString[j] {
21+
match = false
22+
break
23+
}
24+
j += 1
25+
}
26+
if match {
27+
return true
28+
}
29+
30+
i += 1
31+
}
32+
return false
33+
}
34+
}

Sources/XcodeProj/Utils/CommentedString.swift

Lines changed: 151 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,31 @@
11
import Foundation
22

3+
private extension UInt8 {
4+
static let tab: UInt8 = 9 // '\t'
5+
static let newline: UInt8 = 10 // '\n'
6+
static let backslash: UInt8 = 92 // '\'
7+
static let underscore: UInt8 = 95 // '_'
8+
static let doubleQuotes: UInt8 = 34 // '"'
9+
static let dollar: UInt8 = 36 // '$'
10+
static let slash: UInt8 = 47 // '/'
11+
12+
static let dot: UInt8 = 46 // '.'
13+
static let nine: UInt8 = 57 // '9'
14+
15+
static let capitalA: UInt8 = 65 // 'A'
16+
static let capitalZ: UInt8 = 90 // 'Z'
17+
18+
static let smallA: UInt8 = 97 // 'a'
19+
static let smallN: UInt8 = 110 // 'n'
20+
static let smallT: UInt8 = 116 // 't'
21+
static let smallZ: UInt8 = 122 // 'z'
22+
}
23+
24+
private extension ContiguousArray<CChar> {
25+
static let slashesUTF8CString = "//".utf8CString
26+
static let threeUnderscoresUTF8CString = "___".utf8CString
27+
}
28+
329
/// String that includes a comment
430
struct CommentedString {
531
/// Entity string value.
@@ -18,19 +44,6 @@ struct CommentedString {
1844
self.comment = comment
1945
}
2046

21-
/// Set of characters that are invalid.
22-
private static let invalidCharacters: CharacterSet = {
23-
var invalidSet = CharacterSet(charactersIn: "_$")
24-
invalidSet.insert(charactersIn: UnicodeScalar(".") ... UnicodeScalar("9"))
25-
invalidSet.insert(charactersIn: UnicodeScalar("A") ... UnicodeScalar("Z"))
26-
invalidSet.insert(charactersIn: UnicodeScalar("a") ... UnicodeScalar("z"))
27-
invalidSet.invert()
28-
return invalidSet
29-
}()
30-
31-
/// Set of characters that are invalid.
32-
private static let specialCheckCharacters = CharacterSet(charactersIn: "_/")
33-
3447
/// Returns a valid string for Xcode projects.
3548
var validString: String {
3649
switch string {
@@ -40,31 +53,31 @@ struct CommentedString {
4053
default: break
4154
}
4255

43-
if string.rangeOfCharacter(from: CommentedString.invalidCharacters) == nil {
44-
if string.rangeOfCharacter(from: CommentedString.specialCheckCharacters) == nil {
45-
return string
46-
} else if !string.contains("//"), !string.contains("___") {
47-
return string
56+
var str = string
57+
return str.withUTF8 { buffer -> String in
58+
let containsInvalidCharacters = buffer.containsInvalidCharacters
59+
60+
if !containsInvalidCharacters() {
61+
let containsSpecialCheckCharacters = buffer.containsSpecialCheckCharacters()
62+
63+
if !containsSpecialCheckCharacters {
64+
return string
65+
} else if !buffer.containsCString(ContiguousArray.slashesUTF8CString),
66+
!buffer.containsCString(ContiguousArray.threeUnderscoresUTF8CString) {
67+
return string
68+
}
4869
}
49-
}
5070

51-
let escaped = string.reduce(into: "") { escaped, character in
52-
// As an optimization, only look at the first scalar. This means we're doing a numeric comparison instead
53-
// of comparing arbitrary-length characters. This is safe because all our cases are a single scalar.
54-
switch character.unicodeScalars.first {
55-
case "\\":
56-
escaped.append("\\\\")
57-
case "\"":
58-
escaped.append("\\\"")
59-
case "\t":
60-
escaped.append("\\t")
61-
case "\n":
62-
escaped.append("\\n")
63-
default:
64-
escaped.append(character)
71+
// calculate exact size
72+
let escapedCapacity = buffer.escapedCommentCapacity()
73+
74+
// write directly into String storage
75+
return String(unsafeUninitializedCapacity: escapedCapacity) { stringBuffer in
76+
stringBuffer.fillValidString(from: buffer)
77+
78+
return escapedCapacity
6579
}
6680
}
67-
return "\"\(escaped)\""
6881
}
6982
}
7083

@@ -95,3 +108,107 @@ extension CommentedString: ExpressibleByStringLiteral {
95108
self.init(value)
96109
}
97110
}
111+
112+
// MARK: - Private
113+
114+
private extension UnsafeMutableBufferPointer<UInt8> {
115+
/// Fills preallocated `UnsafeBufferPointer<UInt8>`
116+
func fillValidString(from buffer: UnsafeBufferPointer<UInt8>) {
117+
var outIndex = 0
118+
119+
self[outIndex] = .doubleQuotes
120+
outIndex += 1
121+
122+
for character in buffer {
123+
switch character {
124+
case .backslash:
125+
self[outIndex] = .backslash
126+
self[outIndex + 1] = .backslash
127+
outIndex += 2
128+
129+
case .doubleQuotes:
130+
self[outIndex] = .backslash
131+
self[outIndex + 1] = .doubleQuotes
132+
outIndex += 2
133+
134+
case .tab:
135+
self[outIndex] = .backslash
136+
self[outIndex + 1] = .smallT
137+
outIndex += 2
138+
139+
case .newline:
140+
self[outIndex] = .backslash
141+
self[outIndex + 1] = .smallN
142+
outIndex += 2
143+
144+
default:
145+
self[outIndex] = character
146+
outIndex += 1
147+
}
148+
}
149+
150+
self[outIndex] = .doubleQuotes
151+
}
152+
}
153+
154+
private extension UnsafeBufferPointer<UInt8> {
155+
/// Valid characters are:
156+
/// 1. `_` and `$`
157+
/// 2. `.`...`9`
158+
/// 3. `A`...`Z`
159+
/// 4. `a`...`z`
160+
func containsInvalidCharacters() -> Bool {
161+
for character in self {
162+
// character == '_' || character == '$'
163+
if character == .underscore || character == .dollar {
164+
continue
165+
}
166+
// character >= '.' && character <= '9'
167+
if character >= .dot, character <= .nine {
168+
continue
169+
}
170+
// character >= 'A' && character <= 'Z'
171+
if character >= .capitalA, character <= .capitalZ {
172+
continue
173+
}
174+
// character >= 'a' && character <= 'z'
175+
if character >= .smallA, character <= .smallZ {
176+
continue
177+
}
178+
179+
return true
180+
}
181+
182+
return false
183+
}
184+
185+
/// Special check characters are `_` and `/`
186+
func containsSpecialCheckCharacters() -> Bool {
187+
for character in self {
188+
if character == .underscore || character == .slash {
189+
return true
190+
}
191+
}
192+
193+
return false
194+
}
195+
196+
/// Calculates escaped string size
197+
/// Basically, `count + count(where: { [.backslash, .doubleQuotes, .tab, .newline].contains($0) }`
198+
func escapedCommentCapacity() -> Int {
199+
var escapeCount = 0
200+
201+
for character in self {
202+
switch character {
203+
case .backslash, .doubleQuotes, .tab, .newline:
204+
escapeCount += 1 // each adds one extra byte
205+
default:
206+
break
207+
}
208+
}
209+
210+
return count // original bytes
211+
+ escapeCount // extra escape bytes
212+
+ 2 // surrounding quotes
213+
}
214+
}

0 commit comments

Comments
 (0)