File: PlistDecoder.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 (384 lines) | stat: -rw-r--r-- 18,110 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
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2017 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
//
//===----------------------------------------------------------------------===//

internal import _FoundationCShims

//===----------------------------------------------------------------------===//
// Plist Decoder
//===----------------------------------------------------------------------===//

/// `PropertyListDecoder` facilitates the decoding of property list values into semantic `Decodable` types.
// NOTE: older overlays had Foundation.PropertyListDecoder as the ObjC
// name. The two must coexist, so it was renamed. The old name must not
// be used in the new runtime. _TtC10Foundation20_PropertyListDecoder
// is the mangled name for Foundation._PropertyListDecoder.
#if FOUNDATION_FRAMEWORK
@_objcRuntimeName(_TtC10Foundation20_PropertyListDecoder)
#endif
@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *)
open class PropertyListDecoder {
#if FOUNDATION_FRAMEWORK
    public typealias PropertyListFormat = PropertyListSerialization.PropertyListFormat
#else
    public enum PropertyListFormat : UInt, Sendable  {
        case xml = 100
        case binary = 200
        case openStep = 1
    }
#endif
    // MARK: Options

    /// Contextual user-provided information for use during decoding.
    open var userInfo: [CodingUserInfoKey : Any] {
        get {
            optionsLock.lock()
            defer { optionsLock.unlock() }
            return options.userInfo
        }
        _modify {
            optionsLock.lock()
            var value = options.userInfo
            defer {
                options.userInfo = value
                optionsLock.unlock()
            }
            yield &value
        }
        set {
            optionsLock.lock()
            defer { optionsLock.unlock() }
            options.userInfo = newValue
        }
    }

    /// Options set on the top-level encoder to pass down the decoding hierarchy.
    internal struct _Options {
        var userInfo: [CodingUserInfoKey : Any] = [:]
    }

    /// The options set on the top-level decoder.
    fileprivate var options = _Options()
    fileprivate let optionsLock = LockedState<Void>()

    // MARK: - Constructing a Property List Decoder

    /// Initializes `self` with default strategies.
    public init() {}

    // MARK: - Decoding Values

    /// Decodes a top-level value of the given type from the given property list representation.
    ///
    /// - parameter type: The type of the value to decode.
    /// - parameter data: The data to decode from.
    /// - returns: A value of the requested type.
    /// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not a valid property list.
    /// - throws: An error if any value throws an error during decoding.
    open func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T {
        var format: PropertyListDecoder.PropertyListFormat = .binary
        return try decode(type, from: data, format: &format)
    }

    /// Decodes a top-level value of the given type from the given property list representation.
    ///
    /// - parameter type: The type of the value to decode.
    /// - parameter data: The data to decode from.
    /// - parameter format: The parsed property list format.
    /// - returns: A value of the requested type along with the detected format of the property list.
    /// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not a valid property list.
    /// - throws: An error if any value throws an error during decoding.
    open func decode<T : Decodable>(_ type: T.Type, from data: Data, format: inout PropertyListDecoder.PropertyListFormat) throws -> T {
        try _decode({
            try $0.decode(type)
        }, from: data, format: &format)
    }
    
    @available(FoundationPreview 0.1, *)
    open func decode<T : DecodableWithConfiguration>(_ type: T.Type, from data: Data, configuration: T.DecodingConfiguration) throws -> T {
        var format: PropertyListDecoder.PropertyListFormat = .binary
        return try decode(type, from: data, format: &format, configuration: configuration)
    }
    
    @available(FoundationPreview 0.1, *)
    open func decode<T, C>(_ type: T.Type, from data: Data, configuration: C.Type) throws -> T where T : DecodableWithConfiguration, C : DecodingConfigurationProviding, T.DecodingConfiguration == C.DecodingConfiguration {
        try decode(type, from: data, configuration: C.decodingConfiguration)
    }
    
    @available(FoundationPreview 0.1, *)
    open func decode<T, C>(_ type: T.Type, from data: Data, format: inout PropertyListDecoder.PropertyListFormat, configuration: C.Type) throws -> T where T : DecodableWithConfiguration, C: DecodingConfigurationProviding, T.DecodingConfiguration == C.DecodingConfiguration {
        try decode(type, from: data, format: &format, configuration: C.decodingConfiguration)
    }
    
    @available(FoundationPreview 0.1, *)
    open func decode<T : DecodableWithConfiguration>(_ type: T.Type, from data: Data, format: inout PropertyListDecoder.PropertyListFormat, configuration: T.DecodingConfiguration) throws -> T {
        try _decode({
            try $0.decode(type, configuration: configuration)
        }, from: data, format: &format)
    }
    
    private func _decode<T>(_ doDecode: (any _PlistDecoderEntryPointProtocol) throws -> T, from data: Data, format: inout PropertyListDecoder.PropertyListFormat) throws -> T {
        return try Self.detectFormatAndConvertEncoding(for: data, binaryPlist: { utf8Buffer in
            var decoder: _PlistDecoder<_BPlistDecodingFormat>
            do {
                let map = try BPlistScanner.scanBinaryPropertyList(from: utf8Buffer)
                decoder = try _PlistDecoder(referencing: map, options: self.options, codingPathNode: .root)
            } catch let error as BPlistError {
                throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "The given data was not a valid property list.", underlyingError: error.cocoaError))
            }
            let result = try doDecode(decoder)

            let uniquelyReferenced = isKnownUniquelyReferenced(&decoder)
            decoder.takeOwnershipOfBackingDataIfNeeded(selfIsUniquelyReferenced: uniquelyReferenced)

            format = .binary
            return result
        }, xml: { utf8Buffer in
            var decoder: _PlistDecoder<_XMLPlistDecodingFormat>
            do {
                var scanInfo = XMLPlistScanner(buffer: utf8Buffer)
                let map = try scanInfo.scanXMLPropertyList()
                decoder = try _PlistDecoder(referencing: map, options: self.options, codingPathNode: .root)
            } catch let error as XMLPlistError {
                throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "The given data was not a valid property list.", underlyingError: error.cocoaError))
            }
            let result = try doDecode(decoder)

            let uniquelyReferenced = isKnownUniquelyReferenced(&decoder)
            decoder.takeOwnershipOfBackingDataIfNeeded(selfIsUniquelyReferenced: uniquelyReferenced)

            format = .xml
            return result
        }, openstep: { utf16View in
#if FOUNDATION_FRAMEWORK
            let value: Any
            do {
                value = try __ParseOldStylePropertyList(utf16: utf16View)
            } catch let error as OpenStepPlistError {
                throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "The given data was not a valid property list.", underlyingError: error.cocoaError))
            }
            let decoder = __PlistDictionaryDecoder(referencing: value, at: [], options: options)
            format = .openStep
            return try doDecode(decoder)
#else
            // Unsupported until __PlistDictionaryDecoder is available
            throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "The openStep format is unsupported on this platform."))
#endif
        })
    }
    
    @inline(__always)
    private static func findXMLTagOpening(in buffer: BufferView<UInt8>) -> BufferView<UInt8>.Index? {
        buffer.withUnsafeRawPointer { bufPtr, bufCount in
            guard bufCount >= 5 && strncmp(bufPtr, "<?xml", 5) == 0 else {
                return nil
            }
            return buffer.index(buffer.startIndex, offsetBy: 5)
        }
    }
    
    @inline(__always)
    private static func findEncodingLocation(in buffer: BufferView<UInt8>) throws -> BufferView<UInt8>.Index? {
        var idx = buffer.startIndex
        let endIdx = buffer.endIndex
        
        while idx < endIdx {
            let ch = buffer[unchecked: idx]
            
            // Looks like the end of the <?xml...> tag. No explicit encoding found.
            if ch == UInt8(ascii: "?") || ch == UInt8(ascii: ">") {
                return nil
            }

            let subBuffer = buffer[idx...]
            let match = try subBuffer.withUnsafeRawPointer { bufPtr, bufCount -> BufferView<UInt8>.Index? in
                guard bufCount > 9 else {
                    throw DecodingError._dataCorrupted("End of buffer while looking for encoding name", for: .root)
                }
                if strncmp(bufPtr, "encoding=", 9) == 0 {
                    return buffer.index(idx, offsetBy: 9)
                }
                return nil
            }
            if let match {
                return match
            } else {
                buffer.formIndex(after: &idx)
            }
        }
        
        // Reached of the input without finding 'encoding'
        return nil
    }
    
    private static func readQuotedEncoding(in buffer: BufferView<UInt8>) throws -> String.Encoding {
        guard let quote = buffer.first,
              quote == UInt8(ascii: "'") || quote == UInt8(ascii: "\"") else {
            return .utf8
        }

        // Move past the quote character
        let baseIdx = buffer.index(after: buffer.startIndex)
        
        let endIdx = buffer.endIndex
        var idx = baseIdx
        while idx < endIdx && buffer[unchecked: idx] != quote {
            buffer.formIndex(after: &idx)
        }
        
        return try buffer[unchecked: baseIdx..<idx].withUnsafePointer { ptr, encodingLength in
            if encodingLength == 5, _stringshims_strncasecmp_l(ptr, "utf-8", 5, nil) == 0 {
                return .utf8
            }
            
#if FOUNDATION_FRAMEWORK
            guard let encodingName = String(bytes: UnsafeBufferPointer(start: ptr, count: encodingLength), encoding: .isoLatin1) else {
                throw DecodingError._dataCorrupted("Encountered unknown encoding", for: .root)
            }
            let enc = CFStringConvertIANACharSetNameToEncoding(encodingName as CFString)
            guard enc != kCFStringEncodingInvalidId else {
                throw DecodingError._dataCorrupted("Encountered unknown encoding \(encodingName)", for: .root)
            }

            return String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(enc))
#else
            // TODO: For now, FoundationEssentials only has support for utf-8.
            throw DecodingError._dataCorrupted("Encountered unknown encoding", for: .root)
#endif
        }

    }
    
    private static func scanForExplicitXMLEncoding(in buffer: BufferView<UInt8>) throws -> String.Encoding {
        // Scan for the <?xml.... ?> opening
        guard let postOpeningIdx = findXMLTagOpening(in: buffer) else {
            return .utf8
        }
        
        // Found "<?xml"; now we scan for "encoding"
        guard let postEncodingIdx = try findEncodingLocation(in: buffer[postOpeningIdx...]) else {
            return .utf8
        }
        
        // Read the quoted encoding value and convert it into a String.Encoding.
        return try readQuotedEncoding(in: buffer[postEncodingIdx...])
    }

    static func detectEncoding(of buffer: BufferView<UInt8>) throws -> (encoding: String.Encoding, bomLength: Int) {
        // Try detecting BOM first.
        let length = buffer.count
        let byte0 = (length > 0) ? buffer[uncheckedOffset: 0] : nil
        let byte1 = (length > 1) ? buffer[uncheckedOffset: 1] : nil
        let byte2 = (length > 2) ? buffer[uncheckedOffset: 2] : nil
        let byte3 = (length > 3) ? buffer[uncheckedOffset: 3] : nil
        switch (byte0, byte1, byte2, byte3) {
        case (0, 0, 0xFE, 0xFF):
            return (.utf32BigEndian, 4)
        case (0xFE, 0xFF, 0, 0):
            return (.utf32LittleEndian, 4)
        case (0xFE, 0xFF, _, _):
            return (.utf16BigEndian, 2)
        case (0xFF, 0xFE, _, _):
            return (.utf16LittleEndian, 2)
        case (0xEF, 0xBB, 0xBF, _):
            return (.utf8, 3)
        default:
            return try (scanForExplicitXMLEncoding(in: buffer), bomLength: 0)
        }
    }

    static func detectFormatAndConvertEncoding<T>(for data: Data,
                                                  binaryPlist: (BufferView<UInt8>) throws -> T,
                                                  xml: (BufferView<UInt8>) throws -> T,
                                                  openstep: (String.UTF16View) throws -> T) rethrows -> T {
        try data.withBufferView { buffer in
            
            // Binary plist always begins with the same literal bytes, which isn't valid in any of the other formats.
            if BPlistScanner.hasBPlistMagic(in: buffer) {
                return try binaryPlist(buffer)
            }
            
            // Try to deduce the text encoding of the file so that we can determine if it's XML or not.
            let (encoding, bomLength) = try detectEncoding(of: buffer)
            let postBOMIndex = buffer.index(buffer.startIndex, offsetBy: bomLength)
            let postBOMBuffer = buffer[unchecked: postBOMIndex...]
            
            var result: T?
            try Self.withUTF8Representation(of: postBOMBuffer, sourceEncoding: encoding) { utf8Buffer in
                if XMLPlistScanner.detectPossibleXMLPlist(for: utf8Buffer) {
                    result = try xml(utf8Buffer)
                }
            }
            if let result {
                return result
            }
            
            // If it doesn't appear to be XML or binary, then we assume it's an OpenStep plist and try to parse it with that format.
            return try Self.withUTF16Representation(of: postBOMBuffer, sourceEncoding: encoding) { utf16View in
                try openstep(utf16View)
            }
        }
    }

    static func withUTF8Representation<T>(of buffer: BufferView<UInt8>, sourceEncoding: String.Encoding, _ closure: (BufferView<UInt8>) throws -> T ) throws -> T {
        if sourceEncoding == .utf8 {
            return try closure(buffer)
        } else {
            // TODO: This FOUNDATION_FRAMEWORK-only initializer cannot be used by FoundationPreview. Only UTF-8 encoded xml plists are supported there right now. (See readQuotedEncoding(in:).)
            guard var string = String(bytes: buffer, encoding: sourceEncoding) else {
                throw DecodingError._dataCorrupted("Cannot convert input to UTF-8", for: .root)
            }
            return try string.withUTF8 {
                try closure(BufferView(unsafeBufferPointer: $0)!)
            }
        }
    }

    static func withUTF16Representation<T>(of buffer: BufferView<UInt8>, sourceEncoding: String.Encoding, _ closure: (String.UTF16View) throws -> T ) throws -> T {
        // If we were careful with endianness, we could avoid some copies here.
        guard let string = String(bytes: buffer, encoding: sourceEncoding) else {
            throw DecodingError._dataCorrupted("Cannot convert input to UTF-16", for: .root)
        }
        return try closure(string.utf16)
    }

#if FOUNDATION_FRAMEWORK
    // __PlistDictionaryDecoder is only available in the framework for now
    
    /// Decodes a top-level value of the given type from the given property list container (top-level array or dictionary).
    ///
    /// - parameter type: The type of the value to decode.
    /// - parameter container: The top-level plist container.
    /// - returns: A value of the requested type.
    /// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not a valid property list.
    /// - throws: An error if any value throws an error during decoding.
    internal func decode<T : Decodable>(_ type: T.Type, fromTopLevel container: Any) throws -> T {
        let decoder = __PlistDictionaryDecoder(referencing: container, options: self.options)
        guard let value = try decoder.unbox(container, as: type) else {
            throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value."))
        }

        return value
    }
    
    internal func decode<T : DecodableWithConfiguration>(_ type: T.Type, fromTopLevel container: Any, configuration: T.DecodingConfiguration) throws -> T {
        let decoder = __PlistDictionaryDecoder(referencing: container, options: self.options)
        guard let value = try decoder.unbox(container, as: type, configuration: configuration) else {
            throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value."))
        }

        return value
    }
#endif
}

@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
extension PropertyListDecoder : @unchecked Sendable {}