381 lines
18 KiB
Swift

//===----------------------------------------------------------------------===//
//
// 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<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
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<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 {}