mirror of
https://github.com/apple/swift-foundation.git
synced 2025-05-24 23:06:21 +08:00
381 lines
18 KiB
Swift
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 {}
|