File: OpenStepPlist.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (548 lines) | stat: -rw-r--r-- 19,209 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

#if canImport(Darwin)
import Darwin
#elseif os(Android)
import Bionic
#elseif canImport(Glibc)
import Glibc
#elseif os(WASI)
import WASILibc
#elseif canImport(Musl)
import Musl
#endif

#if canImport(CRT)
import CRT
#endif

private struct _ParseInfo {
    let utf16 : String.UTF16View
    var curr : String.UTF16View.Index
    var err: Error?

    mutating func advance() {
        curr = utf16.index(after: curr)
    }

    mutating func retreat() {
        curr = utf16.index(before: curr)
    }

    var currChar : UInt16 {
        utf16[curr]
    }

    var isAtEnd : Bool {
        curr >= utf16.endIndex
    }
}

internal func __ParseOldStylePropertyList(utf16: String.UTF16View) throws -> Any {
    let length = utf16.count
    guard length > 0 else {
        throw OpenStepPlistError("Conversion of string failed. The string is empty.")
    }

    var parseInfo = _ParseInfo(utf16: utf16, curr: utf16.startIndex)

    guard advanceToNonSpace(&parseInfo) else {
        return [String:Any]()
    }

    var result = parsePlistObject(&parseInfo, requireObject: true, depth: 0)
    if result != nil {
        if advanceToNonSpace(&parseInfo) {
            if result is String {
                // Reset info and keep parsing
                parseInfo = _ParseInfo(utf16: utf16, curr: utf16.startIndex)
                result = parsePlistDictContent(&parseInfo, depth: 0)
            } else {
                parseInfo.err = OpenStepPlistError("Junk after plist at line \(lineNumberStrings(parseInfo))")
                result = nil
            }
        }
    }
    guard let result else {
        throw parseInfo.err ?? OpenStepPlistError("Unknown error parsing property list around line \(lineNumberStrings(parseInfo))")
    }
    return result
}

private func parsePlistObject(_ pInfo: inout _ParseInfo, requireObject: Bool, depth: UInt32) -> Any? {
    guard depthIsValid(&pInfo, depth: depth) else {
        return nil
    }

    guard advanceToNonSpace(&pInfo) else {
        if requireObject {
            pInfo.err = OpenStepPlistError("Unexpected EOF while parsing plist")
        }
        return nil
    }

    let ch = pInfo.currChar
    pInfo.advance()
    if ch == UInt16(ascii: "{") {
        return parsePlistDict(&pInfo, depth: depth)
    } else if ch == UInt16(ascii: "(") {
        return parsePlistArray(&pInfo, depth: depth)
    } else if ch == UInt16(ascii: "<") {
        return parsePlistData(&pInfo)
    } else if ch == UInt16(ascii: "'") || ch == UInt16(ascii: "\"") {
        return parseQuotedPlistString(&pInfo, quote: ch)
    } else if isValidUnquotedStringCharacter(ch) {
        pInfo.retreat()
        return parseUnquotedPlistString(&pInfo)
    } else {
        pInfo.retreat() // Must back off the character we just read
        if requireObject {
            pInfo.err = OpenStepPlistError("Unexpected character '0x\(String(ch, radix: 16))' at line \(lineNumberStrings(pInfo))")
        }
        return nil
    }
}

private func parsePlistDict(_ pInfo: inout _ParseInfo, depth: UInt32) -> [String:Any]? {
    guard let dict = parsePlistDictContent(&pInfo, depth: depth) else {
        return nil
    }
    guard advanceToNonSpace(&pInfo) && pInfo.currChar == UInt16(ascii: "}") else {
        pInfo.err = OpenStepPlistError("Expected terminating '}' for dictionary at line \(lineNumberStrings(pInfo))")
        return nil
    }
    pInfo.advance()
    return dict
}

private func parsePlistDictContent(_ pInfo: inout _ParseInfo, depth: UInt32) -> [String:Any]? {
    var dict = [String:Any]()

    while let key = parsePlistString(&pInfo, requireObject: false) {
        guard advanceToNonSpace(&pInfo) else {
            pInfo.err = OpenStepPlistError("Missing ';' on line \(lineNumberStrings(pInfo))")
            return nil
        }

        var value : Any
        if pInfo.currChar == UInt16(ascii: ";") {
            /* This is a strings file using the shortcut format */
            /* although this check here really applies to all plists. */
            value = key
        } else if pInfo.currChar == UInt16(ascii: "=") {
            pInfo.advance()
            guard let v = parsePlistObject(&pInfo, requireObject: true, depth: depth + 1) else {
                return nil
            }
            value = v
        } else {
            pInfo.err = OpenStepPlistError("Expected ';' or '=' ")
            return nil
        }

        dict[key] = value

        guard advanceToNonSpace(&pInfo) && pInfo.currChar == UInt16(ascii: ";") else {
            pInfo.err = OpenStepPlistError("Missing ';' on line \(lineNumberStrings(pInfo))")
            return nil
        }

        pInfo.advance()
    }

    // this is a success path, so clear errors (NOTE: this seems weird, but is historical)
    pInfo.err = nil

    return dict
}

private func parsePlistArray(_ pInfo: inout _ParseInfo, depth: UInt32) -> [Any]? {
    var array = [Any]()

    while let obj = parsePlistObject(&pInfo, requireObject: false, depth: depth + 1) {
        array.append(obj)

        guard advanceToNonSpace(&pInfo) else {
            pInfo.err = OpenStepPlistError("Expected ',' for array at line \(lineNumberStrings(pInfo))")
            return nil
        }

        guard pInfo.currChar == UInt16(ascii: ",") else {
            break
        }

        pInfo.advance()
    }

    guard advanceToNonSpace(&pInfo) && pInfo.currChar == UInt16(ascii: ")") else {
        pInfo.err = OpenStepPlistError("Expected terminating ')' for array at line \(lineNumberStrings(pInfo))")
        return nil
    }

    // this is a success path, so clear errors (NOTE: this seems weird, but is historical)
    pInfo.err = nil

    pInfo.advance() // consume the )
    return array
}

private func parsePlistString(_ pInfo: inout _ParseInfo, requireObject: Bool) -> String? {
    guard advanceToNonSpace(&pInfo) else {
        if requireObject {
            pInfo.err = OpenStepPlistError("Unexpected EOF while parsing string")
        }
        return nil
    }

    let ch = pInfo.currChar
    if ch == UInt16(ascii: "'") || ch == UInt16(ascii: "\"") {
        pInfo.advance()
        return parseQuotedPlistString(&pInfo, quote: ch)
    } else if isValidUnquotedStringCharacter(ch) {
        return parseUnquotedPlistString(&pInfo)
    } else {
        if requireObject {
            pInfo.err = OpenStepPlistError("Invalid string character at line \(lineNumberStrings(pInfo))")
        }
        return nil
    }
}

private func parseQuotedPlistString(_ pInfo: inout _ParseInfo, quote: UInt16) -> String? {
    var result : String?
    let startMark = pInfo.curr
    var mark = startMark
    while !pInfo.isAtEnd {
        let ch = pInfo.currChar
        if ch == quote {
            break
        }
        if ch == UInt16(ascii: "\\") {
            if result == nil {
                result = String()
            }
            result! += String(pInfo.utf16[mark ..< pInfo.curr])!
            pInfo.advance()

            if pInfo.isAtEnd {
                pInfo.err = OpenStepPlistError("Unterminated backslash sequence on line \(lineNumberStrings(pInfo))")
                return nil
            }

            result!.unicodeScalars.append(UnicodeScalar(getSlashedChar(&pInfo))!)
            mark = pInfo.curr
        } else {
            pInfo.advance()
        }
    }
    if pInfo.isAtEnd {
        pInfo.curr = startMark
        pInfo.err = OpenStepPlistError("Unterminated quoted string starting on line \(lineNumberStrings(pInfo))")
        return nil
    }
    if result == nil {
        result = String(pInfo.utf16[mark ..< pInfo.curr])!
    } else if mark != pInfo.curr {
        result! += String(pInfo.utf16[mark ..< pInfo.curr])!
    }

    pInfo.advance() // Advance past the quote character before returning.

    // this is a success path, so clear errors (NOTE: this seems weird, but is historical)
    pInfo.err = nil
    return result
}

private func parseUnquotedPlistString(_ pInfo: inout _ParseInfo) -> String? {
    let mark = pInfo.curr
    while !pInfo.isAtEnd && isValidUnquotedStringCharacter(pInfo.currChar) {
        pInfo.advance()
    }
    if pInfo.curr != mark {
        return String(pInfo.utf16[mark ..< pInfo.curr])!
    }
    pInfo.err = OpenStepPlistError("Unexpected EOF while parsing string")
    return nil
}

private let octalCharRange = UInt16(ascii: "0") ... UInt16(ascii: "7")

private func parseOctal(startingWith ch: UInt16, _ pInfo: inout _ParseInfo) -> UInt16 {
    var num = UInt8( ch &- octalCharRange.lowerBound )

    /* three digits maximum to avoid reading \000 followed by 5 as \5 ! */
    // We've already read the first character here, so repeat at most two more times.
    for _ in 0 ..< 2 {
        // Hitting the end of the plist is not a meaningful error here.
        // We parse the characters we have and allow the parent context (parseQuotedPlistString, the only current call site of getSlashedChar) to produce a more meaningful error message (e.g. it will at least expect a close quote after this character).
        if pInfo.isAtEnd {
            break
        }

        let ch2 = pInfo.currChar
        if octalCharRange ~= ch2 {
            // Note: Like the previous implementation, this `num` value is UInt8, which is smaller than the largest value that can be represented by a three digit octal (0777 = 511). Since this code is compatibility-only, we maintain the truncation behavior that existed with the prior implementation.
            num = (num << 3) &+ (UInt8(ch2) &- UInt8(octalCharRange.lowerBound))
            pInfo.advance()
        } else {
            // Non-octal characters are not explicitly an error either: "\05" is a valid character which evaluates to 005. (We read a '0', a '5', and then a '"'; we can't bail on seeing '"' though.)
            // Is this an ambiguous format? Probably. But it has to remain this way for backwards compatibility.
            // See <rdar://problem/34321354>
            break
        }
    }

    // A probably more expensive replacement for CFStringEncodingBytesToUnicode(kCFStringEncodingNextStepLatin, …)
    guard let str = String(bytes: [num], encoding: .nextstep) else {
        pInfo.err = OpenStepPlistError("Unable to convert octet-stream while parsing plist")
        return 0
    }
    return str.utf16.first ?? 0
}

private func parseU16Scalar(_ pInfo: inout _ParseInfo) -> UInt16 {
    var num : UInt16 = 0
    var numDigits = 4
    while !pInfo.isAtEnd && numDigits > 0 {
        let ch2 = pInfo.currChar
        if ch2 < 128 && isxdigit(Int32(ch2)) != 0 {
            pInfo.advance()
            num = num << 4
            if ch2 <= UInt16(ascii: "9") {
                num += (ch2 &- UInt16(ascii: "0"))
            } else if ch2 <= UInt16(ascii: "F") {
                num += (ch2 &- UInt16(ascii: "A") &+ 10)
            } else {
                num += (ch2 &- UInt16(ascii: "a") &+ 10)
            }
        }
        numDigits -= 1
    }
    return num
}

private func getSlashedChar(_ pInfo: inout _ParseInfo) -> UInt16 {
    let ch = pInfo.currChar
    pInfo.advance()
    switch ch {
    case octalCharRange:
        return parseOctal(startingWith: ch, &pInfo)
    case UInt16(ascii: "U"):
        return parseU16Scalar(&pInfo)
    case UInt16(ascii: "a"): return UInt16(ascii: "\u{7}")
    case UInt16(ascii: "b"): return UInt16(ascii: "\u{8}")
    case UInt16(ascii: "f"): return UInt16(ascii: "\u{12}")
    case UInt16(ascii: "n"): return UInt16(ascii: "\n")
    case UInt16(ascii: "r"): return UInt16(ascii: "\r")
    case UInt16(ascii: "t"): return UInt16(ascii: "\t")
    case UInt16(ascii: "v"): return UInt16(ascii: "\u{11}")
    default:
        return ch
    }
}

private func isValidUnquotedStringCharacter(_ x: UInt16) -> Bool {
    switch x {
        case UInt16(ascii: "a") ... UInt16(ascii: "z"):
            return true
        case UInt16(ascii: "A") ... UInt16(ascii: "Z"):
            return true
        case UInt16(ascii: "0") ... UInt16(ascii: "9"):
            return true
        case UInt16(ascii: "_"), UInt16(ascii: "$"), UInt16(ascii: "/"), UInt16(ascii: ":"), UInt16(ascii: "."), UInt16(ascii: "-"):
            return true
        default:
            return false
    }
}

private func parsePlistData(_ pInfo: inout _ParseInfo) -> Data? {
    var result = Data()

    while true {
        let NUM_BYTES = 400
        let numBytesRead = withUnsafeTemporaryAllocation(of: UInt8.self, capacity: NUM_BYTES) { buffer in
            let numBytesRead = getDataBytes(&pInfo, bytes: buffer)
            if numBytesRead > 0 {
                let subBuffer = buffer[0 ..< numBytesRead]
                result.append(contentsOf: subBuffer)
            }
            return numBytesRead
        }
        guard numBytesRead > 0 else {
            if numBytesRead == -2 {
                pInfo.err = OpenStepPlistError("Malformed data byte group at line  \(lineNumberStrings(pInfo)); uneven length")
                return nil
            } else if numBytesRead < 0 {
                pInfo.err = OpenStepPlistError("Malformed data byte group at line  \(lineNumberStrings(pInfo)); invalid hex")
                return nil
            }
            break
        }
    }

    // this is a success path, so clear errors (NOTE: this seems weird, but is historical)
    pInfo.err = nil

    guard !pInfo.isAtEnd && pInfo.currChar == UInt16(ascii: ">") else {
        pInfo.err = OpenStepPlistError("Expected terminating '>' for data at line  \(lineNumberStrings(pInfo))")
        return nil
    }

    pInfo.advance() // Move past '>'
    return result
}

private func getDataBytes(_ pInfo: inout _ParseInfo, bytes: UnsafeMutableBufferPointer<UInt8>) -> Int {
    var numBytesRead = 0
    while !pInfo.isAtEnd && numBytesRead < bytes.count {
        let ch1 = pInfo.currChar
        if ch1 == UInt16(ascii: ">") { // Meaning we're done
            return numBytesRead
        }

        func fromHexDigit(ch: UInt8) -> UInt8? {
            if isdigit(Int32(ch)) != 0 {
                return ch &- UInt8(ascii: "0")
            }
            if (ch >= UInt8(ascii: "a")) && (ch <= UInt8(ascii: "f")) {
                return ch &- UInt8(ascii: "a") &+ 10
            }
            if (ch >= UInt8(ascii: "A")) && (ch <= UInt8(ascii: "F")) {
                return ch &- UInt8(ascii: "A") &+ 10
            }
            return nil
        }

        if let first = fromHexDigit(ch: UInt8(ch1)) {
            pInfo.advance()
            if pInfo.isAtEnd {
                return -2 // Error: uneven number of hex digits
            }

            let ch2 = pInfo.currChar
            guard let second = fromHexDigit(ch: UInt8(ch2)) else {
                return -2 // Error: uneven number of hex digits
            }

            bytes[numBytesRead] = (first << 4) + second
            numBytesRead += 1
            pInfo.advance()
        } else if ch1 == UInt16(ascii: " ") || ch1 == UInt16(ascii: "\n") || ch1 == UInt16(ascii: "\r") || ch1 == 0x2028 || ch1 == 0x2029 {
            pInfo.advance()
        } else {
            return -1 // Error: unexpected character
        }
    }
    return numBytesRead
}

// Returns true if the advance found something before the end of the buffer, false otherwise
// AFTER-INVARIANT: pInfo->curr <= pInfo->end
//                  However result will be false when pInfo->curr == pInfo->end
private func advanceToNonSpace(_ pInfo: inout _ParseInfo) -> Bool {
    while !pInfo.isAtEnd {
        let ch2 = pInfo.currChar
        pInfo.advance()

        switch ch2 {
            case 0x9, 0xa, 0xb, 0xc, 0xd: continue // tab, newline, vt, form feed, carriage return
            case UInt16(ascii: " "), 0x2028, 0x2029: continue // space and Unicode line sep, para sep
            case UInt16(ascii: "/"):
                if pInfo.isAtEnd {
                    // whoops; back up and return
                    pInfo.retreat()
                    return true
                } else if pInfo.currChar == UInt16(ascii: "/") {
                    pInfo.advance()

                    var atEndOfLine = false
                    while !pInfo.isAtEnd && !atEndOfLine { // go to end of comment line
                        switch pInfo.currChar {
                            case UInt16(ascii: "\n"), UInt16(ascii: "\r"), 0x2028, 0x2029:
                                atEndOfLine = true
                            default:
                                pInfo.advance()
                        }
                    }
                } else if pInfo.currChar == UInt16(ascii: "*") { // handle /* ... */
                    pInfo.advance()

                    while !pInfo.isAtEnd {
                        let ch3 = pInfo.currChar
                        pInfo.advance()
                        if ch3 == UInt16(ascii: "*") && !pInfo.isAtEnd && pInfo.currChar == UInt16(ascii: "/") {
                            pInfo.advance() // advance past the '/'
                            break
                        }
                    }
                } else { // this looked like the start of a comment, but wasn't
                    pInfo.retreat()
                    return true
                }
            default: // this didn't look like a comment, we've found non-whitespace
                pInfo.retreat()
                return true
        }
    }
    return false
}

// when this returns yes, pInfo.err will be set
private func depthIsValid(_ pInfo: inout _ParseInfo, depth: UInt32) -> Bool {
    let MAX_DEPTH = 512
    if depth <= MAX_DEPTH {
        return true
    }
    pInfo.err = OpenStepPlistError("Too many nested arrays or dictionaries at line \(lineNumberStrings(pInfo))")
    return false
}


private func lineNumberStrings(_ pInfo: _ParseInfo) -> Int {
    var p = pInfo.utf16.startIndex
    var count = 1
    while p < pInfo.utf16.endIndex && p < pInfo.curr {
        if pInfo.utf16[p] == UInt16(ascii: "\r") {
            count += 1

            let nextIdx = pInfo.utf16.index(after: p)
            if nextIdx < pInfo.utf16.endIndex && nextIdx < pInfo.curr && pInfo.utf16[nextIdx] == UInt16("\n") {
                p = nextIdx
            }
        } else if pInfo.utf16[p] == UInt16(ascii: "\n") {
            count += 1
        }
        p = pInfo.utf16.index(after: p)
    }
    return count
}

private extension UInt16 {
    init(ascii: UnicodeScalar) {
        self = UInt16(UInt8(ascii: ascii))
    }
}

internal struct OpenStepPlistError: Swift.Error, Equatable {
    var debugDescription : String
    init(_ desc: String) {
        self.debugDescription = desc
    }

    var cocoaError: CocoaError {
        .init(.propertyListReadCorrupt, userInfo: [
            NSDebugDescriptionErrorKey : self.debugDescription
        ])
    }
}