Skip to content

Commit fb60fa3

Browse files
authored
Merge pull request #11081 from itaiferber/4.0-codable_support_for_nsarchival
[4.0] NSKeyed{Una,A}rchiver should support Codable
2 parents 9ee6645 + 83a6362 commit fb60fa3

File tree

4 files changed

+283
-22
lines changed

4 files changed

+283
-22
lines changed

stdlib/public/SDK/Foundation/Codable.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@ internal extension DecodingError {
5151
} else if value is [String : Any] {
5252
return "a dictionary"
5353
} else {
54-
// This should never happen -- we somehow have a non-JSON type here.
55-
preconditionFailure("Invalid storage type \(type(of: value)).")
54+
return "\(type(of: value))"
5655
}
5756
}
5857
}

stdlib/public/SDK/Foundation/NSCoder.swift

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,20 @@ extension NSCoder {
129129
}
130130
}
131131

132+
//===----------------------------------------------------------------------===//
133+
// NSKeyedArchiver
134+
//===----------------------------------------------------------------------===//
135+
136+
extension NSKeyedArchiver {
137+
@nonobjc
138+
@available(OSX 10.11, iOS 9.0, *)
139+
public func encodeEncodable<T : Encodable>(_ value: T, forKey key: String) throws {
140+
let plistEncoder = PropertyListEncoder()
141+
let plist = try plistEncoder.encodeToTopLevelContainer(value)
142+
self.encode(plist, forKey: key)
143+
}
144+
}
145+
132146
//===----------------------------------------------------------------------===//
133147
// NSKeyedUnarchiver
134148
//===----------------------------------------------------------------------===//
@@ -153,6 +167,48 @@ extension NSKeyedUnarchiver {
153167
try resolveError(error)
154168
return result
155169
}
170+
171+
@nonobjc
172+
private static let __plistClasses: [AnyClass] = [
173+
NSArray.self,
174+
NSData.self,
175+
NSDate.self,
176+
NSDictionary.self,
177+
NSNumber.self,
178+
NSString.self
179+
]
180+
181+
@nonobjc
182+
@available(OSX 10.11, iOS 9.0, *)
183+
public func decodeDecodable<T : Decodable>(_ type: T.Type, forKey key: String) -> T? {
184+
guard let value = self.decodeObject(of: NSKeyedUnarchiver.__plistClasses, forKey: key) else {
185+
return nil
186+
}
187+
188+
let plistDecoder = PropertyListDecoder()
189+
do {
190+
return try plistDecoder.decode(T.self, fromTopLevel: value)
191+
} catch {
192+
self.failWithError(error)
193+
return nil
194+
}
195+
}
196+
197+
@nonobjc
198+
@available(OSX 10.11, iOS 9.0, *)
199+
public func decodeTopLevelDecodable<T : Decodable>(_ type: T.Type, forKey key: String) throws -> T? {
200+
guard let value = try self.decodeTopLevelObject(of: NSKeyedUnarchiver.__plistClasses, forKey: key) else {
201+
return nil
202+
}
203+
204+
let plistDecoder = PropertyListDecoder()
205+
do {
206+
return try plistDecoder.decode(T.self, fromTopLevel: value)
207+
} catch {
208+
self.failWithError(error)
209+
throw error;
210+
}
211+
}
156212
}
157213

158214

stdlib/public/SDK/Foundation/PlistEncoder.swift

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,35 @@ open class PropertyListEncoder {
5050
/// - throws: `EncodingError.invalidValue` if a non-comforming floating-point value is encountered during encoding, and the encoding strategy is `.throw`.
5151
/// - throws: An error if any value throws an error during encoding.
5252
open func encode<Value : Encodable>(_ value: Value) throws -> Data {
53+
let topLevel = try encodeToTopLevelContainer(value)
54+
if topLevel is NSNumber {
55+
throw EncodingError.invalidValue(value,
56+
EncodingError.Context(codingPath: [],
57+
debugDescription: "Top-level \(Value.self) encoded as number property list fragment."))
58+
} else if topLevel is NSString {
59+
throw EncodingError.invalidValue(value,
60+
EncodingError.Context(codingPath: [],
61+
debugDescription: "Top-level \(Value.self) encoded as string property list fragment."))
62+
} else if topLevel is NSDate {
63+
throw EncodingError.invalidValue(value,
64+
EncodingError.Context(codingPath: [],
65+
debugDescription: "Top-level \(Value.self) encoded as date property list fragment."))
66+
}
67+
68+
do {
69+
return try PropertyListSerialization.data(fromPropertyList: topLevel, format: self.outputFormat, options: 0)
70+
} catch {
71+
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Unable to encode the given top-level value as a property list", underlyingError: error))
72+
}
73+
}
74+
75+
/// Encodes the given top-level value and returns its plist-type representation.
76+
///
77+
/// - parameter value: The value to encode.
78+
/// - returns: A new top-level array or dictionary representing the value.
79+
/// - throws: `EncodingError.invalidValue` if a non-conforming floating-point value is encountered during encoding, and the encoding strategy is `.throw`.
80+
/// - throws: An error if any value throws an error during encoding.
81+
internal func encodeToTopLevelContainer<Value : Encodable>(_ value: Value) throws -> Any {
5382
let encoder = _PlistEncoder(options: self.options)
5483
try value.encode(to: encoder)
5584

@@ -60,25 +89,7 @@ open class PropertyListEncoder {
6089
}
6190

6291
let topLevel = encoder.storage.popContainer()
63-
if topLevel is NSNumber {
64-
throw EncodingError.invalidValue(value,
65-
EncodingError.Context(codingPath: [],
66-
debugDescription: "Top-level \(Value.self) encoded as number property list fragment."))
67-
} else if topLevel is NSString {
68-
throw EncodingError.invalidValue(value,
69-
EncodingError.Context(codingPath: [],
70-
debugDescription: "Top-level \(Value.self) encoded as string property list fragment."))
71-
} else if topLevel is NSDate {
72-
throw EncodingError.invalidValue(value,
73-
EncodingError.Context(codingPath: [],
74-
debugDescription: "Top-level \(Value.self) encoded as date property list fragment."))
75-
}
76-
77-
do {
78-
return try PropertyListSerialization.data(fromPropertyList: topLevel, format: self.outputFormat, options: 0)
79-
} catch {
80-
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Unable to encode the given top-level value as a property list", underlyingError: error))
81-
}
92+
return topLevel
8293
}
8394
}
8495

@@ -620,7 +631,19 @@ open class PropertyListDecoder {
620631
} catch {
621632
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not a valid property list.", underlyingError: error))
622633
}
623-
let decoder = _PlistDecoder(referencing: topLevel, options: self.options)
634+
635+
return try decode(T.self, fromTopLevel: topLevel)
636+
}
637+
638+
/// Decodes a top-level value of the given type from the given property list container (top-level array or dictionary).
639+
///
640+
/// - parameter type: The type of the value to decode.
641+
/// - parameter container: The top-level plist container.
642+
/// - returns: A value of the requested type.
643+
/// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not a valid property list.
644+
/// - throws: An error if any value throws an error during decoding.
645+
internal func decode<T : Decodable>(_ type: T.Type, fromTopLevel container: Any) throws -> T {
646+
let decoder = _PlistDecoder(referencing: container, options: self.options)
624647
return try T(from: decoder)
625648
}
626649
}

test/stdlib/NSKeyedArchival.swift

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
2+
// Licensed under Apache License v2.0 with Runtime Library Exception
3+
//
4+
// See https://swift.org/LICENSE.txt for license information
5+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
6+
//
7+
//===----------------------------------------------------------------------===//
8+
//
9+
// RUN: %target-run-simple-swift
10+
// REQUIRES: executable_test
11+
// REQUIRES: objc_interop
12+
13+
import Foundation
14+
import StdlibUnittest
15+
16+
// Since we test various apis with and without errors define these conveniences
17+
func expectError(_ verb: () throws -> ()) {
18+
do {
19+
try verb()
20+
expectUnreachable("Expected to throw error!")
21+
} catch {
22+
// expected
23+
}
24+
}
25+
func expectNoError(_ verb: () throws -> ()) {
26+
do {
27+
try verb()
28+
} catch let error {
29+
expectUnreachable("Did not expect to throw error! \(error)")
30+
}
31+
}
32+
33+
// Utilities for minimizing boiler plate in the tests
34+
@available(OSX 10.11, iOS 9.0, *)
35+
func archive(_ archival: (_: NSKeyedArchiver) throws -> Void) rethrows -> Data {
36+
let data = NSMutableData()
37+
let ka = NSKeyedArchiver(forWritingWith: data)
38+
do {
39+
defer { ka.finishEncoding() }
40+
try archival(ka)
41+
}
42+
expectNotEqual(data.length, 0)
43+
return data as Data
44+
}
45+
46+
@available(OSX 10.11, iOS 9.0, *)
47+
func unarchive(_ data: Data, _ unarchival: (_: NSKeyedUnarchiver) throws -> Void) rethrows -> Void {
48+
let ku = NSKeyedUnarchiver(forReadingWith: data as Data)
49+
ku.requiresSecureCoding = true
50+
ku.decodingFailurePolicy = .setErrorAndReturn
51+
try unarchival(ku)
52+
ku.finishDecoding()
53+
}
54+
55+
56+
// MARK: - Data
57+
// simple struct comprised of entirely plist types
58+
struct Simple : Codable, Equatable {
59+
var number: Int = 5
60+
var string: String = "hello"
61+
var data: Data = Data(bytes: [0, 1, 2, 3])
62+
var array: [String] = ["stay", "a while", "and listen"]
63+
var dictionary: [String:String] = ["Deckard": "Cain"]
64+
static func ==(lhs: Simple, rhs: Simple) -> Bool {
65+
return lhs.number == rhs.number && lhs.string == rhs.string && lhs.data == rhs.data && lhs.array == rhs.array && lhs.dictionary == rhs.dictionary
66+
}
67+
}
68+
69+
// simple struct comprised of entirely plist types
70+
struct ThrowingCodable : Codable {
71+
public func encode(to encoder: Encoder) throws {
72+
throw NSError(domain: "sad", code: 3)
73+
}
74+
}
75+
76+
// MARK - Tests
77+
@available(OSX 10.11, iOS 9.0, *)
78+
func test_simpleCodableSupport() {
79+
let s = Simple()
80+
81+
var data: Data? = nil
82+
expectNoError {
83+
data = try archive { archiver in
84+
try archiver.encodeEncodable(s, forKey: "valid")
85+
try archiver.encodeEncodable(s, forKey: "wrong-type")
86+
}
87+
}
88+
89+
unarchive(data!) { unarchiver in
90+
// confirm we can roundtrip our data
91+
let roundtrip = unarchiver.decodeDecodable(Simple.self, forKey: "valid")
92+
expectNotNil(roundtrip)
93+
if let rt = roundtrip {
94+
expectEqual(rt, s)
95+
}
96+
97+
// also ask for something that is not there
98+
let notThere = unarchiver.decodeDecodable(Simple.self, forKey: "not-there")
99+
expectNil(notThere)
100+
expectNil(unarchiver.error)
101+
102+
// String != Simple so this should fail at the type level
103+
let wrongType = unarchiver.decodeDecodable(String.self, forKey: "wrong-type")
104+
expectNil(wrongType)
105+
expectNotNil(unarchiver.error)
106+
}
107+
}
108+
109+
@available(OSX 10.11, iOS 9.0, *)
110+
func test_encodableErrorHandling() {
111+
expectError {
112+
_ = try archive { archiver in
113+
try archiver.encodeEncodable(ThrowingCodable(), forKey: "non-codable")
114+
}
115+
}
116+
}
117+
118+
@available(OSX 10.11, iOS 9.0, *)
119+
func test_readingNonCodableFromDecodeDecodable() {
120+
var data: Data? = nil
121+
expectNoError {
122+
data = archive { archiver in
123+
archiver.encode(NSDate(), forKey: "non-codable")
124+
}
125+
}
126+
127+
unarchive(data!) { unarchiver in
128+
let nonCodable = unarchiver.decodeDecodable(Simple.self, forKey: "non-codable")
129+
expectNil(nonCodable)
130+
expectNotNil(unarchiver.error)
131+
}
132+
}
133+
134+
@available(OSX 10.11, iOS 9.0, *)
135+
func test_toplevelAPIVariants() {
136+
let s = Simple()
137+
var data: Data? = nil
138+
expectNoError {
139+
data = try archive { archiver in
140+
try archiver.encodeEncodable(s, forKey: "valid")
141+
try archiver.encodeEncodable(Date(), forKey: "non-codable")
142+
}
143+
}
144+
145+
unarchive(data!) { unarchiver in
146+
var caught = false
147+
do {
148+
_ = try unarchiver.decodeTopLevelDecodable(Simple.self, forKey: "non-codable")
149+
} catch {
150+
caught = true
151+
}
152+
expectTrue(caught)
153+
}
154+
155+
expectNoError {
156+
_ = try unarchive(data!) { unarchiver in
157+
let roundtrip = try unarchiver.decodeTopLevelDecodable(Simple.self, forKey: "valid")
158+
expectNotNil(roundtrip)
159+
if let rt = roundtrip {
160+
expectEqual(rt, s)
161+
}
162+
}
163+
}
164+
}
165+
166+
// MARK: - Run Tests
167+
168+
#if !FOUNDATION_XCTEST
169+
if #available(OSX 10.11, iOS 9.0, *) {
170+
let NSKeyedArchiverTest = TestSuite("TestNSKeyedArchiver")
171+
let tests = [
172+
"NSKeyedArchival.simpleCodableSupportInNSKeyedArchival": test_simpleCodableSupport,
173+
"NSKeyedArchival.encodableErrorHandling": test_encodableErrorHandling,
174+
"NSKeyedArchival.readingNonCodableFromDecodeDecodable": test_readingNonCodableFromDecodeDecodable,
175+
"NSKeyedArchival.toplevelAPIVariants": test_toplevelAPIVariants
176+
]
177+
178+
for (name, test) in tests {
179+
NSKeyedArchiverTest.test(name) { test() }
180+
}
181+
runAllTests()
182+
}
183+
#endif

0 commit comments

Comments
 (0)