Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite plutil for parity with all Darwin functionality #5172

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
12 changes: 12 additions & 0 deletions Sources/Foundation/NSNumber.swift
Original file line number Diff line number Diff line change
@@ -1150,6 +1150,18 @@ open class NSNumber : NSValue, @unchecked Sendable {
}

open override var classForCoder: AnyClass { return NSNumber.self }

/// 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.
@_spi(BooleanCheckingForPLUtil)
public var _exactBoolValue: Bool? {
if self === kCFBooleanTrue {
return true
} else if self === kCFBooleanFalse {
return false
} else {
return nil
}
}
}

extension CFNumber : _NSBridgeable {
6 changes: 5 additions & 1 deletion Sources/plutil/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -13,7 +13,11 @@
##===----------------------------------------------------------------------===##

add_executable(plutil
main.swift)
main.swift
PLUContext_Arguments.swift
PLUContext_KeyPaths.swift
PLUContext.swift
PLULiteralOutput.swift)

target_link_libraries(plutil PRIVATE
Foundation)
1,173 changes: 1,173 additions & 0 deletions Sources/plutil/PLUContext.swift

Large diffs are not rendered by default.

73 changes: 73 additions & 0 deletions Sources/plutil/PLUContext_Arguments.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 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
//
//===----------------------------------------------------------------------===//

import Foundation

/// Common arguments for create, insert, extract, etc.
struct PLUContextArguments {
var paths: [String]
var readable: Bool
var terminatingNewline: Bool
var outputFileName: String?
var outputFileExtension: String?
var silent: Bool?

init(arguments: [String]) throws {
paths = []
readable = false
terminatingNewline = true

var argumentIterator = arguments.makeIterator()
var readRemainingAsPaths = false
while let arg = argumentIterator.next() {
switch arg {
case "--":
readRemainingAsPaths = true
break
case "-n":
terminatingNewline = false
case "-s":
silent = true
case "-r":
readable = true
case "-o":
guard let next = argumentIterator.next() else {
throw PLUContextError.argument("Missing argument for -o.")
}

outputFileName = next
case "-e":
guard let next = argumentIterator.next() else {
throw PLUContextError.argument("Missing argument for -e.")
}

outputFileExtension = next
default:
if arg.hasPrefix("-") && arg.count > 1 {
throw PLUContextError.argument("unrecognized option: \(arg)")
}
paths.append(arg)
}
}

if readRemainingAsPaths {
while let arg = argumentIterator.next() {
paths.append(arg)
}
}

// Make sure we have files
guard !paths.isEmpty else {
throw PLUContextError.argument("No files specified.")
}
}
}
207 changes: 207 additions & 0 deletions Sources/plutil/PLUContext_KeyPaths.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 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
//
//===----------------------------------------------------------------------===//

extension String {
/// 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 `.`.
internal func escapedKeyPathSplit() -> [String] {
let escapesReplaced = self.replacing("\\.", with: "A_DOT_WAS_HERE")
let split = escapesReplaced.split(separator: ".", omittingEmptySubsequences: false)
return split.map { $0.replacingOccurrences(of: "A_DOT_WAS_HERE", with: ".") }
}
}

extension [String] {
/// Re-create an escaped string, if any of the components contain a `.`.
internal func escapedKeyPathJoin() -> String {
let comps = self.map { $0.replacingOccurrences(of: ".", with: "\\.") }
let joined = comps.joined(separator: ".")
return joined
}
}

// MARK: - Get Value at Key Path

func value(atKeyPath: String, in propertyList: Any) -> Any? {
let comps = atKeyPath.escapedKeyPathSplit()
return _value(atKeyPath: comps, in: propertyList, remainingKeyPath: comps[comps.startIndex..<comps.endIndex])
}

func _value(atKeyPath: [String], in propertyList: Any, remainingKeyPath: ArraySlice<String>) -> Any? {
if remainingKeyPath.isEmpty {
// We're there
return propertyList
}

guard let key = remainingKeyPath.first, !key.isEmpty else {
return nil
}

if let dictionary = propertyList as? [String: Any] {
if let dictionaryValue = dictionary[key] {
return _value(atKeyPath: atKeyPath, in: dictionaryValue, remainingKeyPath: remainingKeyPath.dropFirst())
} else {
return nil
}
} else if let array = propertyList as? [Any] {
if let lastInt = Int(key), (array.startIndex..<array.endIndex).contains(lastInt) {
return _value(atKeyPath: atKeyPath, in: array[lastInt], remainingKeyPath: remainingKeyPath.dropFirst())
} else {
return nil
}
}

return nil
}

// MARK: - Remove Value At Key Path

func removeValue(atKeyPath: String, in propertyList: Any) throws -> Any? {
let comps = atKeyPath.escapedKeyPathSplit()
return try _removeValue(atKeyPath: comps, in: propertyList, remainingKeyPath: comps[comps.startIndex..<comps.endIndex])
}

func _removeValue(atKeyPath: [String], in propertyList: Any, remainingKeyPath: ArraySlice<String>) throws -> Any? {
if remainingKeyPath.isEmpty {
// We're there
return nil
}

guard let key = remainingKeyPath.first, !key.isEmpty else {
throw PLUContextError.argument("No value to remove at key path \(atKeyPath.escapedKeyPathJoin())")
}

if let dictionary = propertyList as? [String: Any] {
guard let existing = dictionary[String(key)] else {
throw PLUContextError.argument("No value to remove at key path \(atKeyPath.escapedKeyPathJoin())")
}

var new = dictionary
if let removed = try _removeValue(atKeyPath: atKeyPath, in: existing, remainingKeyPath: remainingKeyPath.dropFirst()) {
new[key] = removed
} else {
new.removeValue(forKey: key)
}
return new
} else if let array = propertyList as? [Any] {
guard let intKey = Int(key), (array.startIndex..<array.endIndex).contains(intKey) else {
throw PLUContextError.argument("No value to remove at key path \(atKeyPath.escapedKeyPathJoin())")
}

let existing = array[intKey]

var new = array
if let removed = try _removeValue(atKeyPath: atKeyPath, in: existing, remainingKeyPath: remainingKeyPath.dropFirst()) {
new[intKey] = removed
} else {
new.remove(at: intKey)
}
return new
} else {
// Cannot descend further into the property list, but we have keys remaining in the path
throw PLUContextError.argument("No value to remove at key path \(atKeyPath.escapedKeyPathJoin())")
}
}

// MARK: - Insert or Replace Value At Key Path

func insertValue(_ value: Any, atKeyPath: String, in propertyList: Any, replacing: Bool, appending: Bool) throws -> Any {
let comps = atKeyPath.escapedKeyPathSplit()
return try _insertValue(value, atKeyPath: comps, in: propertyList, remainingKeyPath: comps[comps.startIndex..<comps.endIndex], replacing: replacing, appending: appending)
}

func _insertValue(_ value: Any, atKeyPath: [String], in propertyList: Any, remainingKeyPath: ArraySlice<String>, replacing: Bool, appending: Bool) throws -> Any {
// Are we recursing further, or is this the place where we are inserting?
guard let key = remainingKeyPath.first else {
throw PLUContextError.argument("Key path not found \(atKeyPath.escapedKeyPathJoin())")
}

if let dictionary = propertyList as? [String : Any] {
let existingValue = dictionary[key]
if remainingKeyPath.count > 1 {
// Descend
if let existingValue {
var new = dictionary
new[key] = try _insertValue(value, atKeyPath: atKeyPath, in: existingValue, remainingKeyPath: remainingKeyPath.dropFirst(), replacing: replacing, appending: appending)
return new
} else {
throw PLUContextError.argument("Key path not found \(atKeyPath.escapedKeyPathJoin())")
}
} else {
// Insert
if replacing {
// Just slam it in
var new = dictionary
new[key] = value
return new
} else if let existingValue {
if appending {
if var existingValueArray = existingValue as? [Any] {
existingValueArray.append(value)
var new = dictionary
new[key] = existingValueArray
return new
} else {
throw PLUContextError.argument("Appending to a non-array at key path \(atKeyPath.escapedKeyPathJoin())")
}
} else {
// Not replacing, already exists, not appending to an array
throw PLUContextError.argument("Value already exists at key path \(atKeyPath.escapedKeyPathJoin())")
}
} else {
// Still just slam it in
var new = dictionary
new[key] = value
return new
}
}
} else if let array = propertyList as? [Any] {
guard let intKey = Int(key) else {
throw PLUContextError.argument("Unable to index into array with key path \(atKeyPath.escapedKeyPathJoin())")
}

let containsKey = array.indices.contains(intKey)

if remainingKeyPath.count > 1 {
// Descend
if containsKey {
var new = array
new[intKey] = try _insertValue(value, atKeyPath: atKeyPath, in: array[intKey], remainingKeyPath: remainingKeyPath.dropFirst(), replacing: replacing, appending: appending)
return new
} else {
throw PLUContextError.argument("Index \(intKey) out of bounds in array at key path \(atKeyPath.escapedKeyPathJoin())")
}
} else {
if appending {
// Append to the array in this array, at this index
guard let valueAtKey = array[intKey] as? [Any] else {
throw PLUContextError.argument("Attempt to append value to non-array at key path \(atKeyPath.escapedKeyPathJoin())")
}
var new = array
new[intKey] = valueAtKey + [value]
return new
} else if containsKey {
var new = array
new.insert(value, at: intKey)
return new
} else if intKey == array.count {
// note: the value of the integer can be out of bounds for the array (== the endIndex). We treat that as an append.
var new = array
new.append(value)
return new
} else {
throw PLUContextError.argument("Index \(intKey) out of bounds in array at key path \(atKeyPath.escapedKeyPathJoin())")
}
}
} else {
throw PLUContextError.argument("Unable to insert value at key path \(atKeyPath.escapedKeyPathJoin())")
}
}
287 changes: 287 additions & 0 deletions Sources/plutil/PLULiteralOutput.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 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
//
//===----------------------------------------------------------------------===//

import Foundation

internal func swiftLiteralDataWithPropertyList(_ plist: Any, originalFileName: String) throws -> Data {
let string = try _swiftLiteralDataWithPropertyList(plist, depth: 0, indent: true, originalFilename: originalFileName)

let withNewline = string.appending("\n")
return withNewline.data(using: .utf8)!
}

internal func objcLiteralDataWithPropertyList(_ plist: Any, originalFileName: String, newFileName: String) throws -> Data {
let string = try _objcLiteralDataWithPropertyList(plist, depth: 0, indent: true, originalFilename: originalFileName, outputFilename: newFileName)

let withNewline = string.appending(";\n")
return withNewline.data(using: .utf8)!
}

internal func objcLiteralHeaderDataWithPropertyList(_ plist: Any, originalFileName: String, newFileName: String) throws -> Data {
let result = try _objCLiteralVaribleWithPropertyList(plist, forHeader: true, originalFilename: originalFileName, outputFilename: newFileName)

// Add final semi-colon
let withNewline = result.appending(";\n")
return withNewline.data(using: .utf8)!
}

internal enum LiteralFormat {
case swift
case objc
}

internal func propertyListIsValidForLiteralFormat(_ plist: Any, format: LiteralFormat) -> Bool {
switch format {
case .swift:
return PropertyListSerialization.propertyList(plist, isValidFor: .binary)
case .objc:
if let _ = plist as? String {
return true
} else if let _ = plist as? NSNumber {
return true
} else if let array = plist as? [Any] {
for item in array {
if !propertyListIsValidForLiteralFormat(item, format: format) {
return false
}
}
return true
} else if let dictionary = plist as? [AnyHashable: Any] {
for (key, value) in dictionary {
if !propertyListIsValidForLiteralFormat(key, format: format) {
return false
}
if !propertyListIsValidForLiteralFormat(value, format: format) {
return false
}
}
return true
} else {
return false
}
}
}

// MARK: - Helpers

internal func _indentation(forDepth depth: Int, numberOfSpaces: Int = 4) -> String {
var result = ""
for _ in 0..<depth {
let spaces = repeatElement(Character(" "), count: numberOfSpaces)
result.append(contentsOf: spaces)
}
return result
}

private func varName(from file: String) -> String {
let filenameStem = file.stem
var varName = filenameStem.replacingOccurrences(of: "-", with: "_").replacingOccurrences(of: " ", with: "_")
let invalidChars = CharacterSet.symbols.union(.controlCharacters)
while let contained = varName.rangeOfCharacter(from: invalidChars) {
varName.removeSubrange(contained)
}
return varName
}

extension String {
fileprivate var escapedForQuotesAndEscapes: String {
var result = self
let knownCommonEscapes = ["\\b", "\\s", "\"", "\\w", "\\.", "\\|", "\\*", "\\)", "\\("]

for escape in knownCommonEscapes {
result = result.replacingOccurrences(of: escape, with: "\\\(escape)")
}

return result
}
}

// MARK: - ObjC

private func _objcLiteralDataWithPropertyList(_ plist: Any, depth: Int, indent: Bool, originalFilename: String, outputFilename: String) throws -> String {
var result = ""
if depth == 0 {
result.append(try _objCLiteralVaribleWithPropertyList(plist, forHeader: false, originalFilename: originalFilename, outputFilename: outputFilename))
}

if indent {
result.append(_indentation(forDepth: depth))
}

if let num = plist as? NSNumber {
return result.appending(try num.propertyListFormatted(objCStyle: true))
} else if let string = plist as? String {
return result.appending("@\"\(string.escapedForQuotesAndEscapes)\"")
} else if let array = plist as? [Any] {
result.append("@[\n")
for element in array {
result.append( try _objcLiteralDataWithPropertyList(element, depth: depth + 1, indent: true, originalFilename: originalFilename, outputFilename: outputFilename))
result.append(",\n")
}
result.append(_indentation(forDepth: depth))
result.append("]")
} else if let dictionary = plist as? [String : Any] {
result.append("@{\n")
let sortedKeys = Array(dictionary.keys).sorted(by: sortDictionaryKeys)

for key in sortedKeys {
result.append(_indentation(forDepth: depth + 1))
result.append("@\"\(key)\" : ")
let value = dictionary[key]!
let valueString = try _objcLiteralDataWithPropertyList(value, depth: depth + 1, indent: false, originalFilename: originalFilename, outputFilename: outputFilename)
result.append("\(valueString),\n")
}
result.append(_indentation(forDepth: depth))
result.append("}")
} else {
throw PLUContextError.invalidPropertyListObject("Objective-C literal syntax does not support classes of type \(type(of: plist))")
}
return result
}

private func _objCLiteralVaribleWithPropertyList(_ plist: Any, forHeader: Bool, originalFilename: String, outputFilename: String) throws -> String {
let objCName: String
if let _ = plist as? NSNumber {
objCName = "NSNumber"
} else if let _ = plist as? String {
objCName = "NSString"
} else if let _ = plist as? [Any] {
objCName = "NSArray"
} else if let _ = plist as? [AnyHashable : Any] {
objCName = "NSDictionary"
} else {
throw PLUContextError.invalidPropertyListObject("Objective-C literal syntax does not support classes of type \(type(of: plist))")
}

var result = ""
if forHeader {
result.append("#import <Foundation/Foundation.h>\n\n")
} else if outputFilename != "-" {
// Don't emit for stdout
result.append("#import \"\(outputFilename.lastComponent?.stem ?? "").h\"\n\n")
}


result.append("/// Generated from \(originalFilename.lastComponent ?? "a file")\n")

// The most common usage will be to generate things that aren't exposed to others via a public header. We default to hidden visibility so as to avoid unintended exported symbols.
result.append("__attribute__((visibility(\"hidden\")))\n")

if forHeader {
result.append("extern ")
}

result.append("\(objCName) * const \(varName(from: originalFilename))")

if !forHeader {
result.append(" = ")
}

return result
}

// MARK: - Swift

private func _swiftLiteralDataWithPropertyList(_ plist: Any, depth: Int, indent: Bool, originalFilename: String) throws -> String {
var result = ""
if depth == 0 {
result.append("/// Generated from \(originalFilename.lastComponent ?? "a file")\n")
// Previous implementation would attempt to determine dynamically if the type annotation was by checking if there was a collection of different types. For now, this just always adds it.
result.append("let \(varName(from: originalFilename))")

// Dictionaries and Arrays need to check for specific type annotation, in case they contain different types. Other types do not.
if let dictionary = plist as? [String: Any] {
var lastType: PlutilExpectType?
var needsAnnotation = false
for (_, value) in dictionary {
if let lastType {
if lastType != PlutilExpectType(propertyList: value) {
needsAnnotation = true
break
}
} else {
lastType = PlutilExpectType(propertyList: value)
}
}

if needsAnnotation {
result.append(" : [String : Any]")
}
} else if let array = plist as? [Any] {
var lastType: PlutilExpectType?
var needsAnnotation = false
for value in array {
if let lastType {
if lastType != PlutilExpectType(propertyList: value) {
needsAnnotation = true
break
}
} else {
lastType = PlutilExpectType(propertyList: value)
}
}

if needsAnnotation {
result.append(" : [Any]")
}
}

result.append(" = ")
}

if indent {
result.append(_indentation(forDepth: depth))
}

if let num = plist as? NSNumber {
result.append(try num.propertyListFormatted(objCStyle: false))
} else if let string = plist as? String {
// FEATURE: Support triple-quote when string is multi-line.
// For now, do one simpler thing and replace newlines with literal \n
let escaped = string.escapedForQuotesAndEscapes.replacingOccurrences(of: "\n", with: "\\n")
result.append("\"\(escaped)\"")
} else if let array = plist as? [Any] {
result.append("[\n")
for element in array {
result.append( try _swiftLiteralDataWithPropertyList(element, depth: depth + 1, indent: true, originalFilename: originalFilename))
result.append(",\n")
}
result.append(_indentation(forDepth: depth))
result.append("]")
} else if let dictionary = plist as? [String : Any] {
result.append("[\n")
let sortedKeys = Array(dictionary.keys).sorted(by: sortDictionaryKeys)

for key in sortedKeys {
result.append(_indentation(forDepth: depth + 1))
result.append("\"\(key)\" : ")
let value = dictionary[key]!
let valueString = try _swiftLiteralDataWithPropertyList(value, depth: depth + 1, indent: false, originalFilename: originalFilename)
result.append("\(valueString),\n")
}
result.append(_indentation(forDepth: depth))
result.append("]")
} else if let data = plist as? Data {
result.append("Data(bytes: [")
for byte in data {
result.append(String(format: "0x%02X", byte))
result.append(",")
}
result.append("])")
} else if let date = plist as? Date {
result.append("Date(timeIntervalSinceReferenceDate: \(date.timeIntervalSinceReferenceDate))")
} else {
throw PLUContextError.invalidPropertyListObject("Swift literal syntax does not support classes of type \(type(of: plist))")
}
return result
}

413 changes: 24 additions & 389 deletions Sources/plutil/main.swift

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Tests/Foundation/TestProcess.swift
Original file line number Diff line number Diff line change
@@ -563,7 +563,7 @@ class TestProcess : XCTestCase {
task.arguments = []
let stdoutPipe = Pipe()
let stdoutData = Mutex(Data())
task.standardOutput = stdoutPipe
task.standardError = stdoutPipe

stdoutPipe.fileHandleForReading.readabilityHandler = { fh in
stdoutData.withLock {