//===----------------------------------------------------------------------===// // // 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. @preconcurrency open var userInfo: [CodingUserInfoKey : any Sendable] { 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 Sendable] = [:] } /// The options set on the top-level decoder. fileprivate var options = _Options() fileprivate let optionsLock = LockedState() // 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(_ 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(_ 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(_ 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(_ 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(_ 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(_ 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(_ 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 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) }) } @inline(__always) private static func findXMLTagOpening(in buffer: BufferView) -> BufferView.Index? { buffer.withUnsafeRawPointer { bufPtr, bufCount in guard bufCount >= 5 && strncmp(bufPtr, ") throws -> BufferView.Index? { var idx = buffer.startIndex let endIdx = buffer.endIndex while idx < endIdx { let ch = buffer[unchecked: idx] // Looks like the end of the 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.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) 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..) throws -> String.Encoding { // Scan for the opening guard let postOpeningIdx = findXMLTagOpening(in: buffer) else { return .utf8 } // Found ") 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(for data: Data, binaryPlist: (BufferView) throws -> T, xml: (BufferView) 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(of buffer: BufferView, sourceEncoding: String.Encoding, _ closure: (BufferView) 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(of buffer: BufferView, 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(_ 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(_ 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 {}