11import 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
430struct 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