Skip to content

Commit ad782e2

Browse files
authored
Rewrite plutil for parity with all Darwin functionality (#5172)
* Rewrite plutil for parity with all Darwin functionality * Fix implementation of stem * Allow non-collection types to be used as Swift output * Fix behavior of no-argument invocation to use lint * Fixes for ObjC literal output * Fix plutil build by including CoreFoundation * add CoreFoundation requirement to cmake file * Work around need to use CoreFoundation directly from plutil * Fix up TestProcess to check stderr for plutil output instead of stdout * Use FileHandle for better cross-platform stdout writing
1 parent c23cc23 commit ad782e2

File tree

8 files changed

+1782
-391
lines changed

8 files changed

+1782
-391
lines changed

Sources/Foundation/NSNumber.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,6 +1150,18 @@ open class NSNumber : NSValue, @unchecked Sendable {
11501150
}
11511151

11521152
open override var classForCoder: AnyClass { return NSNumber.self }
1153+
1154+
/// Provides a way for `plutil` to know if `CFPropertyList` has returned a literal `true`/`false` value, as opposed to a number which happens to have a value of 1 or 0.
1155+
@_spi(BooleanCheckingForPLUtil)
1156+
public var _exactBoolValue: Bool? {
1157+
if self === kCFBooleanTrue {
1158+
return true
1159+
} else if self === kCFBooleanFalse {
1160+
return false
1161+
} else {
1162+
return nil
1163+
}
1164+
}
11531165
}
11541166

11551167
extension CFNumber : _NSBridgeable {

Sources/plutil/CMakeLists.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313
##===----------------------------------------------------------------------===##
1414

1515
add_executable(plutil
16-
main.swift)
16+
main.swift
17+
PLUContext_Arguments.swift
18+
PLUContext_KeyPaths.swift
19+
PLUContext.swift
20+
PLULiteralOutput.swift)
1721

1822
target_link_libraries(plutil PRIVATE
1923
Foundation)

Sources/plutil/PLUContext.swift

Lines changed: 1173 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
15+
/// Common arguments for create, insert, extract, etc.
16+
struct PLUContextArguments {
17+
var paths: [String]
18+
var readable: Bool
19+
var terminatingNewline: Bool
20+
var outputFileName: String?
21+
var outputFileExtension: String?
22+
var silent: Bool?
23+
24+
init(arguments: [String]) throws {
25+
paths = []
26+
readable = false
27+
terminatingNewline = true
28+
29+
var argumentIterator = arguments.makeIterator()
30+
var readRemainingAsPaths = false
31+
while let arg = argumentIterator.next() {
32+
switch arg {
33+
case "--":
34+
readRemainingAsPaths = true
35+
break
36+
case "-n":
37+
terminatingNewline = false
38+
case "-s":
39+
silent = true
40+
case "-r":
41+
readable = true
42+
case "-o":
43+
guard let next = argumentIterator.next() else {
44+
throw PLUContextError.argument("Missing argument for -o.")
45+
}
46+
47+
outputFileName = next
48+
case "-e":
49+
guard let next = argumentIterator.next() else {
50+
throw PLUContextError.argument("Missing argument for -e.")
51+
}
52+
53+
outputFileExtension = next
54+
default:
55+
if arg.hasPrefix("-") && arg.count > 1 {
56+
throw PLUContextError.argument("unrecognized option: \(arg)")
57+
}
58+
paths.append(arg)
59+
}
60+
}
61+
62+
if readRemainingAsPaths {
63+
while let arg = argumentIterator.next() {
64+
paths.append(arg)
65+
}
66+
}
67+
68+
// Make sure we have files
69+
guard !paths.isEmpty else {
70+
throw PLUContextError.argument("No files specified.")
71+
}
72+
}
73+
}
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
extension String {
14+
/// Key paths can contain a `.`, but it must be escaped with a backslash `\.`. This function splits up a keypath, honoring the ability to escape a `.`.
15+
internal func escapedKeyPathSplit() -> [String] {
16+
let escapesReplaced = self.replacing("\\.", with: "A_DOT_WAS_HERE")
17+
let split = escapesReplaced.split(separator: ".", omittingEmptySubsequences: false)
18+
return split.map { $0.replacingOccurrences(of: "A_DOT_WAS_HERE", with: ".") }
19+
}
20+
}
21+
22+
extension [String] {
23+
/// Re-create an escaped string, if any of the components contain a `.`.
24+
internal func escapedKeyPathJoin() -> String {
25+
let comps = self.map { $0.replacingOccurrences(of: ".", with: "\\.") }
26+
let joined = comps.joined(separator: ".")
27+
return joined
28+
}
29+
}
30+
31+
// MARK: - Get Value at Key Path
32+
33+
func value(atKeyPath: String, in propertyList: Any) -> Any? {
34+
let comps = atKeyPath.escapedKeyPathSplit()
35+
return _value(atKeyPath: comps, in: propertyList, remainingKeyPath: comps[comps.startIndex..<comps.endIndex])
36+
}
37+
38+
func _value(atKeyPath: [String], in propertyList: Any, remainingKeyPath: ArraySlice<String>) -> Any? {
39+
if remainingKeyPath.isEmpty {
40+
// We're there
41+
return propertyList
42+
}
43+
44+
guard let key = remainingKeyPath.first, !key.isEmpty else {
45+
return nil
46+
}
47+
48+
if let dictionary = propertyList as? [String: Any] {
49+
if let dictionaryValue = dictionary[key] {
50+
return _value(atKeyPath: atKeyPath, in: dictionaryValue, remainingKeyPath: remainingKeyPath.dropFirst())
51+
} else {
52+
return nil
53+
}
54+
} else if let array = propertyList as? [Any] {
55+
if let lastInt = Int(key), (array.startIndex..<array.endIndex).contains(lastInt) {
56+
return _value(atKeyPath: atKeyPath, in: array[lastInt], remainingKeyPath: remainingKeyPath.dropFirst())
57+
} else {
58+
return nil
59+
}
60+
}
61+
62+
return nil
63+
}
64+
65+
// MARK: - Remove Value At Key Path
66+
67+
func removeValue(atKeyPath: String, in propertyList: Any) throws -> Any? {
68+
let comps = atKeyPath.escapedKeyPathSplit()
69+
return try _removeValue(atKeyPath: comps, in: propertyList, remainingKeyPath: comps[comps.startIndex..<comps.endIndex])
70+
}
71+
72+
func _removeValue(atKeyPath: [String], in propertyList: Any, remainingKeyPath: ArraySlice<String>) throws -> Any? {
73+
if remainingKeyPath.isEmpty {
74+
// We're there
75+
return nil
76+
}
77+
78+
guard let key = remainingKeyPath.first, !key.isEmpty else {
79+
throw PLUContextError.argument("No value to remove at key path \(atKeyPath.escapedKeyPathJoin())")
80+
}
81+
82+
if let dictionary = propertyList as? [String: Any] {
83+
guard let existing = dictionary[String(key)] else {
84+
throw PLUContextError.argument("No value to remove at key path \(atKeyPath.escapedKeyPathJoin())")
85+
}
86+
87+
var new = dictionary
88+
if let removed = try _removeValue(atKeyPath: atKeyPath, in: existing, remainingKeyPath: remainingKeyPath.dropFirst()) {
89+
new[key] = removed
90+
} else {
91+
new.removeValue(forKey: key)
92+
}
93+
return new
94+
} else if let array = propertyList as? [Any] {
95+
guard let intKey = Int(key), (array.startIndex..<array.endIndex).contains(intKey) else {
96+
throw PLUContextError.argument("No value to remove at key path \(atKeyPath.escapedKeyPathJoin())")
97+
}
98+
99+
let existing = array[intKey]
100+
101+
var new = array
102+
if let removed = try _removeValue(atKeyPath: atKeyPath, in: existing, remainingKeyPath: remainingKeyPath.dropFirst()) {
103+
new[intKey] = removed
104+
} else {
105+
new.remove(at: intKey)
106+
}
107+
return new
108+
} else {
109+
// Cannot descend further into the property list, but we have keys remaining in the path
110+
throw PLUContextError.argument("No value to remove at key path \(atKeyPath.escapedKeyPathJoin())")
111+
}
112+
}
113+
114+
// MARK: - Insert or Replace Value At Key Path
115+
116+
func insertValue(_ value: Any, atKeyPath: String, in propertyList: Any, replacing: Bool, appending: Bool) throws -> Any {
117+
let comps = atKeyPath.escapedKeyPathSplit()
118+
return try _insertValue(value, atKeyPath: comps, in: propertyList, remainingKeyPath: comps[comps.startIndex..<comps.endIndex], replacing: replacing, appending: appending)
119+
}
120+
121+
func _insertValue(_ value: Any, atKeyPath: [String], in propertyList: Any, remainingKeyPath: ArraySlice<String>, replacing: Bool, appending: Bool) throws -> Any {
122+
// Are we recursing further, or is this the place where we are inserting?
123+
guard let key = remainingKeyPath.first else {
124+
throw PLUContextError.argument("Key path not found \(atKeyPath.escapedKeyPathJoin())")
125+
}
126+
127+
if let dictionary = propertyList as? [String : Any] {
128+
let existingValue = dictionary[key]
129+
if remainingKeyPath.count > 1 {
130+
// Descend
131+
if let existingValue {
132+
var new = dictionary
133+
new[key] = try _insertValue(value, atKeyPath: atKeyPath, in: existingValue, remainingKeyPath: remainingKeyPath.dropFirst(), replacing: replacing, appending: appending)
134+
return new
135+
} else {
136+
throw PLUContextError.argument("Key path not found \(atKeyPath.escapedKeyPathJoin())")
137+
}
138+
} else {
139+
// Insert
140+
if replacing {
141+
// Just slam it in
142+
var new = dictionary
143+
new[key] = value
144+
return new
145+
} else if let existingValue {
146+
if appending {
147+
if var existingValueArray = existingValue as? [Any] {
148+
existingValueArray.append(value)
149+
var new = dictionary
150+
new[key] = existingValueArray
151+
return new
152+
} else {
153+
throw PLUContextError.argument("Appending to a non-array at key path \(atKeyPath.escapedKeyPathJoin())")
154+
}
155+
} else {
156+
// Not replacing, already exists, not appending to an array
157+
throw PLUContextError.argument("Value already exists at key path \(atKeyPath.escapedKeyPathJoin())")
158+
}
159+
} else {
160+
// Still just slam it in
161+
var new = dictionary
162+
new[key] = value
163+
return new
164+
}
165+
}
166+
} else if let array = propertyList as? [Any] {
167+
guard let intKey = Int(key) else {
168+
throw PLUContextError.argument("Unable to index into array with key path \(atKeyPath.escapedKeyPathJoin())")
169+
}
170+
171+
let containsKey = array.indices.contains(intKey)
172+
173+
if remainingKeyPath.count > 1 {
174+
// Descend
175+
if containsKey {
176+
var new = array
177+
new[intKey] = try _insertValue(value, atKeyPath: atKeyPath, in: array[intKey], remainingKeyPath: remainingKeyPath.dropFirst(), replacing: replacing, appending: appending)
178+
return new
179+
} else {
180+
throw PLUContextError.argument("Index \(intKey) out of bounds in array at key path \(atKeyPath.escapedKeyPathJoin())")
181+
}
182+
} else {
183+
if appending {
184+
// Append to the array in this array, at this index
185+
guard let valueAtKey = array[intKey] as? [Any] else {
186+
throw PLUContextError.argument("Attempt to append value to non-array at key path \(atKeyPath.escapedKeyPathJoin())")
187+
}
188+
var new = array
189+
new[intKey] = valueAtKey + [value]
190+
return new
191+
} else if containsKey {
192+
var new = array
193+
new.insert(value, at: intKey)
194+
return new
195+
} else if intKey == array.count {
196+
// note: the value of the integer can be out of bounds for the array (== the endIndex). We treat that as an append.
197+
var new = array
198+
new.append(value)
199+
return new
200+
} else {
201+
throw PLUContextError.argument("Index \(intKey) out of bounds in array at key path \(atKeyPath.escapedKeyPathJoin())")
202+
}
203+
}
204+
} else {
205+
throw PLUContextError.argument("Unable to insert value at key path \(atKeyPath.escapedKeyPathJoin())")
206+
}
207+
}

0 commit comments

Comments
 (0)