Skip to content

Commit 9d47821

Browse files
committed
Implement two missing features from UserDefaults:
- Volatile domains. - UserDefaults.argumentDomain, a particular volatile domain, including arguments parsing. WIP: Requires testing.
1 parent 5a6f4c6 commit 9d47821

File tree

5 files changed

+201
-32
lines changed

5 files changed

+201
-32
lines changed

Foundation.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
1520469B1D8AEABE00D02E36 /* HTTPServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1520469A1D8AEABE00D02E36 /* HTTPServer.swift */; };
1313
153E951120111DC500F250BE /* CFKnownLocations.h in Headers */ = {isa = PBXBuildFile; fileRef = 153E950F20111DC500F250BE /* CFKnownLocations.h */; settings = {ATTRIBUTES = (Private, ); }; };
1414
153E951220111DC500F250BE /* CFKnownLocations.c in Sources */ = {isa = PBXBuildFile; fileRef = 153E951020111DC500F250BE /* CFKnownLocations.c */; };
15+
153E95162012A29900F250BE /* UserDefaults_Arguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 153E95152012A29900F250BE /* UserDefaults_Arguments.swift */; };
1516
159884921DCC877700E3314C /* TestHTTPCookieStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 159884911DCC877700E3314C /* TestHTTPCookieStorage.swift */; };
1617
231503DB1D8AEE5D0061694D /* TestDecimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231503DA1D8AEE5D0061694D /* TestDecimal.swift */; };
1718
294E3C1D1CC5E19300E4F44C /* TestNSAttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 294E3C1C1CC5E19300E4F44C /* TestNSAttributedString.swift */; };
@@ -502,6 +503,7 @@
502503
1520469A1D8AEABE00D02E36 /* HTTPServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPServer.swift; sourceTree = "<group>"; };
503504
153E950F20111DC500F250BE /* CFKnownLocations.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CFKnownLocations.h; sourceTree = "<group>"; };
504505
153E951020111DC500F250BE /* CFKnownLocations.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = CFKnownLocations.c; sourceTree = "<group>"; };
506+
153E95152012A29900F250BE /* UserDefaults_Arguments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults_Arguments.swift; sourceTree = "<group>"; };
505507
159884911DCC877700E3314C /* TestHTTPCookieStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHTTPCookieStorage.swift; sourceTree = "<group>"; };
506508
22B9C1E01C165D7A00DECFF9 /* TestDate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestDate.swift; sourceTree = "<group>"; };
507509
231503DA1D8AEE5D0061694D /* TestDecimal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestDecimal.swift; sourceTree = "<group>"; };
@@ -1793,6 +1795,7 @@
17931795
isa = PBXGroup;
17941796
children = (
17951797
EADE0B871BD15DFF00C49C64 /* UserDefaults.swift */,
1798+
153E95152012A29900F250BE /* UserDefaults_Arguments.swift */,
17961799
5BDC3F3B1BCC5DCB00ED97BB /* NSLocale.swift */,
17971800
5BD70FB11D3D4CDC003B9BF8 /* Locale.swift */,
17981801
);
@@ -2191,6 +2194,7 @@
21912194
61E0117E1C1B55B9000037DD /* Timer.swift in Sources */,
21922195
EADE0BCD1BD15E0000C49C64 /* XMLParser.swift in Sources */,
21932196
5BDC3FD01BCF17E600ED97BB /* NSCFSet.swift in Sources */,
2197+
153E95162012A29900F250BE /* UserDefaults_Arguments.swift in Sources */,
21942198
5B1FD9DE1D6D16580080E83C /* TaskRegistry.swift in Sources */,
21952199
EADE0B931BD15DFF00C49C64 /* NSComparisonPredicate.swift in Sources */,
21962200
5B1FD9DC1D6D16580080E83C /* URLSessionDelegate.swift in Sources */,

Foundation/UserDefaults.swift

Lines changed: 103 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,62 @@
99

1010
import CoreFoundation
1111

12-
private var registeredDefaults = [String: Any]()
12+
private var registeredDefaults = [String: NSObject]()
1313
private var sharedDefaults = UserDefaults()
1414

15+
internal func plistValueAsNSObject(_ value: Any) -> NSObject? {
16+
let nsValue: NSObject
17+
18+
// Converts a value to the internal representation. Internalized values are
19+
// stored as NSObject derived objects in the registration dictionary.
20+
if let val = value as? String {
21+
nsValue = val._nsObject
22+
} else if let val = value as? URL {
23+
nsValue = val.path._nsObject
24+
} else if let val = value as? Int {
25+
nsValue = NSNumber(value: val)
26+
} else if let val = value as? Double {
27+
nsValue = NSNumber(value: val)
28+
} else if let val = value as? Bool {
29+
nsValue = NSNumber(value: val)
30+
} else if let val = value as? Data {
31+
nsValue = val._nsObject
32+
} else if let val = value as? NSObject {
33+
nsValue = val
34+
} else {
35+
return nil
36+
}
37+
38+
return nsValue
39+
}
40+
41+
private extension Dictionary {
42+
func convertingValuesToNSObjects() -> [Key: NSObject]? {
43+
var result: [Key: NSObject] = [:]
44+
45+
for (key, value) in self {
46+
if let nsValue = plistValueAsNSObject(value) {
47+
result[key] = nsValue
48+
} else {
49+
return nil
50+
}
51+
}
52+
53+
return result
54+
}
55+
}
56+
57+
private extension Dictionary where Value == NSObject {
58+
mutating func merge(convertingValuesToNSObject source: [Key: Any], uniquingKeysWith block: (NSObject, NSObject) throws -> Value) rethrows -> Bool {
59+
if let converted = source.convertingValuesToNSObjects() {
60+
try self.merge(converted, uniquingKeysWith: block)
61+
return true
62+
} else {
63+
return false
64+
}
65+
}
66+
}
67+
1568
open class UserDefaults: NSObject {
1669
private let suite: String?
1770

@@ -31,9 +84,17 @@ open class UserDefaults: NSObject {
3184
/// nil suite means use the default search list that +standardUserDefaults uses
3285
public init?(suiteName suitename: String?) {
3386
suite = suitename
87+
super.init()
88+
89+
setVolatileDomain(UserDefaults._parsedArgumentsDomain, forName: UserDefaults.argumentDomain)
3490
}
3591

3692
open func object(forKey defaultName: String) -> Any? {
93+
let argumentDomain = volatileDomain(forName: UserDefaults.argumentDomain)
94+
if let object = argumentDomain[defaultName] {
95+
return object
96+
}
97+
3798
func getFromRegistered() -> Any? {
3899
return registeredDefaults[defaultName]
39100
}
@@ -265,30 +326,8 @@ open class UserDefaults: NSObject {
265326
}
266327

267328
open func register(defaults registrationDictionary: [String : Any]) {
268-
for (key, value) in registrationDictionary {
269-
let nsValue: NSObject
270-
271-
// Converts a value to the internal representation. Internalized values are
272-
// stored as NSObject derived objects in the registration dictionary.
273-
if let val = value as? String {
274-
nsValue = val._nsObject
275-
} else if let val = value as? URL {
276-
nsValue = val.path._nsObject
277-
} else if let val = value as? Int {
278-
nsValue = NSNumber(value: val)
279-
} else if let val = value as? Double {
280-
nsValue = NSNumber(value: val)
281-
} else if let val = value as? Bool {
282-
nsValue = NSNumber(value: val)
283-
} else if let val = value as? Data {
284-
nsValue = val._nsObject
285-
} else if let val = value as? NSObject {
286-
nsValue = val
287-
} else {
288-
fatalError("The type of 'value' passed to UserDefaults.register(defaults:) is not supported.")
289-
}
290-
291-
registeredDefaults[key] = nsValue
329+
if !registeredDefaults.merge(convertingValuesToNSObject: registrationDictionary, uniquingKeysWith: { $1 }) {
330+
fatalError("The type of 'value' passed to UserDefaults.register(defaults:) is not supported.")
292331
}
293332
}
294333

@@ -307,16 +346,50 @@ open class UserDefaults: NSObject {
307346
var allDefaults = registeredDefaults
308347

309348
for (key, value) in bPref {
310-
allDefaults[key._swiftObject] = value
349+
if let value = plistValueAsNSObject(value) {
350+
allDefaults[key._swiftObject] = value
351+
}
311352
}
312353

313354
return allDefaults
314355
}
315356

316-
open var volatileDomainNames: [String] { NSUnimplemented() }
317-
open func volatileDomain(forName domainName: String) -> [String : Any] { NSUnimplemented() }
318-
open func setVolatileDomain(_ domain: [String : Any], forName domainName: String) { NSUnimplemented() }
319-
open func removeVolatileDomain(forName domainName: String) { NSUnimplemented() }
357+
private static let _parsedArgumentsDomain: [String: Any] = UserDefaults._parseArguments(ProcessInfo.processInfo.arguments)
358+
359+
private var _volatileDomains: [String: [String: NSObject]] = [:]
360+
private let _volatileDomainsLock = NSLock()
361+
362+
open var volatileDomainNames: [String] {
363+
_volatileDomainsLock.lock()
364+
let names = Array(_volatileDomains.keys)
365+
_volatileDomainsLock.unlock()
366+
367+
return names
368+
}
369+
370+
open func volatileDomain(forName domainName: String) -> [String : Any] {
371+
_volatileDomainsLock.lock()
372+
let domain = _volatileDomains[domainName]
373+
_volatileDomainsLock.unlock()
374+
375+
return domain ?? [:]
376+
}
377+
378+
open func setVolatileDomain(_ domain: [String : Any], forName domainName: String) {
379+
_volatileDomainsLock.lock()
380+
var convertedDomain: [String: NSObject] = _volatileDomains[domainName] ?? [:]
381+
if !convertedDomain.merge(convertingValuesToNSObject: domain, uniquingKeysWith: { $1 }) {
382+
fatalError("The type of 'value' passed to UserDefaults.setVolatileDomain(_:forName:) is not supported.")
383+
}
384+
_volatileDomains[domainName] = convertedDomain
385+
_volatileDomainsLock.unlock()
386+
}
387+
388+
open func removeVolatileDomain(forName domainName: String) {
389+
_volatileDomainsLock.lock()
390+
_volatileDomains.removeValue(forKey: domainName)
391+
_volatileDomainsLock.unlock()
392+
}
320393

321394
open func persistentDomain(forName domainName: String) -> [String : Any]? { NSUnimplemented() }
322395
open func setPersistentDomain(_ domain: [String : Any], forName domainName: String) { NSUnimplemented() }
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// This source file is part of the Swift.org open source project
2+
//
3+
// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
//
6+
// See http://swift.org/LICENSE.txt for license information
7+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
8+
//
9+
10+
import CoreFoundation
11+
12+
fileprivate let propertyListPrefixes: Set<Character> = [ "{", "[", "(", "<", "\"" ]
13+
14+
internal extension UserDefaults {
15+
static func _parseArguments(_ arguments: [String]) -> [String: Any] {
16+
var result: [String: Any] = [:]
17+
18+
let count = arguments.count
19+
20+
var index = 0
21+
while index < count - 1 { // We're looking for pairs, so stop at the second-to-last argument.
22+
let current = arguments[index]
23+
let next = arguments[index + 1]
24+
if current.hasPrefix("-") && !next.hasPrefix("-") {
25+
// Match what Darwin does, which is to check whether the first argument is one of the characters that make up a NeXTStep-style or XML property list: open brace, open parens, open bracket, open angle bracket, or double quote. If it is, attempt parsing it as a plist; otherwise, just use the argument value as a String.
26+
27+
let keySubstring = current[current.index(after: current.startIndex)...]
28+
if !keySubstring.isEmpty {
29+
let key = String(keySubstring)
30+
let value = next.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
31+
32+
var parsed = false
33+
if let prefix = value.first, propertyListPrefixes.contains(prefix) {
34+
if let data = value.data(using: .utf8),
35+
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil),
36+
let plistNS = plistValueAsNSObject(plist) {
37+
38+
// If we can parse that argument as a plist, use the parsed value.
39+
parsed = true
40+
result[key] = plistNS
41+
42+
}
43+
}
44+
45+
if !parsed, let valueNS = plistValueAsNSObject(value) {
46+
result[key] = valueNS
47+
}
48+
}
49+
50+
index += 1 // Skip both the key and the value on this loop.
51+
}
52+
53+
index += 1
54+
}
55+
56+
return result
57+
}
58+
}

TestFoundation/TestUserDefaults.swift

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
//
99

1010
#if DEPLOYMENT_RUNTIME_OBJC || os(Linux)
11-
import Foundation
11+
@testable import Foundation
1212
import XCTest
1313
#else
14-
import SwiftFoundation
14+
@testable import SwiftFoundation
1515
import SwiftXCTest
1616
#endif
1717

@@ -38,6 +38,7 @@ class TestUserDefaults : XCTestCase {
3838
("test_setValue_BoolFromString", test_setValue_BoolFromString ),
3939
("test_setValue_IntFromString", test_setValue_IntFromString ),
4040
("test_setValue_DoubleFromString", test_setValue_DoubleFromString ),
41+
("test_parseArguments", test_parseArguments),
4142
]
4243
}
4344

@@ -254,4 +255,36 @@ class TestUserDefaults : XCTestCase {
254255

255256
XCTAssertEqual(defaults.double(forKey: "key1"), 12.34)
256257
}
258+
259+
func test_parseArguments() {
260+
var shouldBeEmpty: [String: Any]
261+
262+
shouldBeEmpty = UserDefaults._parseArguments([])
263+
XCTAssert(shouldBeEmpty.isEmpty)
264+
265+
shouldBeEmpty = UserDefaults._parseArguments([ "There are", "no arguments", "here that", "should be", "parsed into", "stuff", "-wowThisIsTheLastAndShouldNotProduceAKey"])
266+
XCTAssert(shouldBeEmpty.isEmpty)
267+
268+
XCTAssertEqual(UserDefaults._parseArguments([ "-SomeDefault", "SomeValue"]) as! [String: String], ["SomeDefault": "SomeValue"])
269+
270+
XCTAssertEqual(UserDefaults._parseArguments([ "-SomeDefault", "SomeValue", "-Whoa", "1234", "This isn't parsed", "-WhoaAgain", "2345", "-wowThisIsTheLastAndShouldNotProduceAKey"]) as! [String: String], [
271+
"SomeDefault": "SomeValue",
272+
"Whoa": "1234",
273+
"WhoaAgain": "2345",
274+
])
275+
276+
XCTAssertEqual(UserDefaults._parseArguments([ "-SomeDefault", "(\"SomeValue\")"]) as! [String: [String]], ["SomeDefault": [ "SomeValue" ]])
277+
XCTAssertEqual(UserDefaults._parseArguments([ "-SomeDefault", "{\"SomeKey\" = \"SomeValue\";}"]) as! [String: [String: String]], ["SomeDefault": [ "SomeKey": "SomeValue" ]])
278+
XCTAssertEqual(UserDefaults._parseArguments([ "-SomeDefault", "\"SomeValue\"" ]) as! [String: String], ["SomeDefault": "SomeValue"])
279+
280+
let result = UserDefaults._parseArguments([ "-SomeDefault1", "(\"SomeValue1\", \"SomeValue2\")",
281+
"-SomeDefault2", "{\"SomeKey\" = \"SomeValue\";}",
282+
"This isn't parsed",
283+
"-SomeDefault3", "\"SomeValue\"",
284+
"-wowThisIsTheLastAndShouldNotProduceAKey" ])
285+
XCTAssertEqual(result.count, 3)
286+
XCTAssertEqual(result["SomeDefault1"] as! [String], [ "SomeValue1", "SomeValue2" ])
287+
XCTAssertEqual(result["SomeDefault2"] as! [String: String], [ "SomeKey": "SomeValue" ])
288+
XCTAssertEqual(result["SomeDefault3"] as! String, "SomeValue")
289+
}
257290
}

build.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@
446446
'Foundation/URLSession/libcurl/libcurlHelpers.swift',
447447
'Foundation/URLSession/http/HTTPURLProtocol.swift',
448448
'Foundation/UserDefaults.swift',
449+
'Foundation/UserDefaults_Arguments.swift',
449450
'Foundation/NSUUID.swift',
450451
'Foundation/NSValue.swift',
451452
'Foundation/XMLDocument.swift',

0 commit comments

Comments
 (0)